KKcorps commented on code in PR #19434:
URL: https://github.com/apache/pinot/pull/19434#discussion_r3956615196


##########
pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufUtils.java:
##########
@@ -60,9 +81,53 @@ public static File getFileCopiedToLocal(String filePath)
     }
   }
 
+  /// Returns the content of the descriptor file at the given path. A remote 
file is fetched fresh on every call so
+  /// in-place updates are picked up, and the last successfully fetched (and 
parseable) content is remembered per
+  /// URI: when the fetch fails, or returns bytes that do not parse as a 
descriptor set, the remembered copy is
+  /// served instead so that decoder creation survives transient DNS / 
object-store outages. Local files are always
+  /// read fresh and never remembered.
+  ///
+  /// NOTE: Only descriptor files get this fallback. The jar used by 
[ProtoBufCodeGenMessageDecoder] is downloaded
+  /// via [#getFileCopiedToLocal(String)] without one (see the note there).
   public static InputStream getDescriptorFileInputStream(String 
descriptorFilePath)
       throws Exception {
-    return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    URI fileURI = URI.create(descriptorFilePath);
+    String scheme = fileURI.getScheme();
+    if (scheme == null || scheme.equals(PinotFSFactory.LOCAL_PINOT_FS_SCHEME)) 
{
+      return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    }
+    byte[] content;
+    try {
+      content = downloadFileToBytes(descriptorFilePath);
+      // Validate before remembering so that a corrupt/truncated download can 
neither be served nor overwrite the
+      // last known good copy
+      DynamicSchema.parseFrom(new ByteArrayInputStream(content));
+      LAST_KNOWN_GOOD_DESCRIPTORS.put(descriptorFilePath, content);
+    } catch (Exception e) {
+      content = LAST_KNOWN_GOOD_DESCRIPTORS.getIfPresent(descriptorFilePath);
+      if (content == null) {
+        throw e;
+      }
+      LOGGER.warn("Failed to fetch protocol buffer descriptor file: {}, 
falling back to the last successfully"
+          + " fetched copy", descriptorFilePath, e);
+    }
+    return new ByteArrayInputStream(content);
+  }
+
+  private static byte[] downloadFileToBytes(String filePath)
+      throws Exception {
+    File localFile = getFileCopiedToLocal(filePath);
+    try {
+      return Files.readAllBytes(localFile.toPath());
+    } finally {
+      Files.deleteIfExists(localFile.toPath());
+      Files.deleteIfExists(localFile.getParentFile().toPath());

Review Comment:
   **[Critical] Cleanup breaks descriptor loads through Hadoop-backed 
filesystems.** With Hadoop 3.4.3, `copyToLocalFile()` writes a hidden checksum 
sidecar (for example, `.descriptor.desc.crc`) beside the local descriptor. This 
block deletes only `localFile`, then attempts to delete its still-nonempty 
parent, which throws `DirectoryNotEmptyException`. I reproduced that exception 
here, so a cold HDFS load fails; if the cache is warm, every successful refresh 
instead falls back to stale bytes. Please avoid the local-copy lifecycle for 
descriptor reads—`PinotFS.open(URI)` can be consumed with try-with-resources—or 
ensure the entire temp tree is safely owned and removed. Please add coverage 
using a checksum-producing filesystem.



##########
pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufUtils.java:
##########
@@ -60,9 +81,53 @@ public static File getFileCopiedToLocal(String filePath)
     }
   }
 
+  /// Returns the content of the descriptor file at the given path. A remote 
file is fetched fresh on every call so
+  /// in-place updates are picked up, and the last successfully fetched (and 
parseable) content is remembered per
+  /// URI: when the fetch fails, or returns bytes that do not parse as a 
descriptor set, the remembered copy is
+  /// served instead so that decoder creation survives transient DNS / 
object-store outages. Local files are always
+  /// read fresh and never remembered.
+  ///
+  /// NOTE: Only descriptor files get this fallback. The jar used by 
[ProtoBufCodeGenMessageDecoder] is downloaded
+  /// via [#getFileCopiedToLocal(String)] without one (see the note there).
   public static InputStream getDescriptorFileInputStream(String 
descriptorFilePath)
       throws Exception {
-    return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    URI fileURI = URI.create(descriptorFilePath);
+    String scheme = fileURI.getScheme();
+    if (scheme == null || scheme.equals(PinotFSFactory.LOCAL_PINOT_FS_SCHEME)) 
{
+      return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    }
+    byte[] content;
+    try {
+      content = downloadFileToBytes(descriptorFilePath);
+      // Validate before remembering so that a corrupt/truncated download can 
neither be served nor overwrite the
+      // last known good copy
+      DynamicSchema.parseFrom(new ByteArrayInputStream(content));

Review Comment:
   **[Major] This is not sufficient validation for cache promotion.** 
`DynamicSchema.parseFrom()` accepts an empty descriptor set; I verified that 
`new byte[0]` produces a schema with no message types. The following `put` can 
therefore replace a valid cached descriptor with unusable bytes, after which 
the caller fails while resolving its configured message type. Please promote 
bytes only after the actual decoder validates the required descriptor/message 
(or supply that validation through a callback). A regression test should warm a 
good entry, return an empty descriptor, and verify that the good fallback 
remains intact.



##########
pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufUtils.java:
##########
@@ -60,9 +81,53 @@ public static File getFileCopiedToLocal(String filePath)
     }
   }
 
+  /// Returns the content of the descriptor file at the given path. A remote 
file is fetched fresh on every call so
+  /// in-place updates are picked up, and the last successfully fetched (and 
parseable) content is remembered per
+  /// URI: when the fetch fails, or returns bytes that do not parse as a 
descriptor set, the remembered copy is
+  /// served instead so that decoder creation survives transient DNS / 
object-store outages. Local files are always
+  /// read fresh and never remembered.
+  ///
+  /// NOTE: Only descriptor files get this fallback. The jar used by 
[ProtoBufCodeGenMessageDecoder] is downloaded
+  /// via [#getFileCopiedToLocal(String)] without one (see the note there).
   public static InputStream getDescriptorFileInputStream(String 
descriptorFilePath)
       throws Exception {
-    return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    URI fileURI = URI.create(descriptorFilePath);
+    String scheme = fileURI.getScheme();
+    if (scheme == null || scheme.equals(PinotFSFactory.LOCAL_PINOT_FS_SCHEME)) 
{
+      return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    }
+    byte[] content;
+    try {
+      content = downloadFileToBytes(descriptorFilePath);
+      // Validate before remembering so that a corrupt/truncated download can 
neither be served nor overwrite the
+      // last known good copy
+      DynamicSchema.parseFrom(new ByteArrayInputStream(content));
+      LAST_KNOWN_GOOD_DESCRIPTORS.put(descriptorFilePath, content);

Review Comment:
   **[Major] Concurrent successful refreshes can roll this cache backward.** 
There is no ordering between fetch completion and this unconditional `put`: 
request A can fetch old bytes and pause, request B can fetch and cache a newer 
descriptor, and then A can overwrite it with the old value. A later outage 
serves the older schema. A deterministic probe reproduced `old fetch -> new 
fetch -> old fallback`. Please serialize refreshes per URI or use a 
generation/version-aware publication rule, with a concurrent regression test.



##########
pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufUtils.java:
##########
@@ -60,9 +81,53 @@ public static File getFileCopiedToLocal(String filePath)
     }
   }
 
+  /// Returns the content of the descriptor file at the given path. A remote 
file is fetched fresh on every call so
+  /// in-place updates are picked up, and the last successfully fetched (and 
parseable) content is remembered per
+  /// URI: when the fetch fails, or returns bytes that do not parse as a 
descriptor set, the remembered copy is
+  /// served instead so that decoder creation survives transient DNS / 
object-store outages. Local files are always
+  /// read fresh and never remembered.
+  ///
+  /// NOTE: Only descriptor files get this fallback. The jar used by 
[ProtoBufCodeGenMessageDecoder] is downloaded
+  /// via [#getFileCopiedToLocal(String)] without one (see the note there).
   public static InputStream getDescriptorFileInputStream(String 
descriptorFilePath)
       throws Exception {
-    return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    URI fileURI = URI.create(descriptorFilePath);
+    String scheme = fileURI.getScheme();
+    if (scheme == null || scheme.equals(PinotFSFactory.LOCAL_PINOT_FS_SCHEME)) 
{
+      return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    }
+    byte[] content;
+    try {
+      content = downloadFileToBytes(descriptorFilePath);
+      // Validate before remembering so that a corrupt/truncated download can 
neither be served nor overwrite the
+      // last known good copy
+      DynamicSchema.parseFrom(new ByteArrayInputStream(content));
+      LAST_KNOWN_GOOD_DESCRIPTORS.put(descriptorFilePath, content);
+    } catch (Exception e) {

Review Comment:
   **[Major] Please do not treat descriptor validation failures as transient 
fetch outages.** This catch covers both remote I/O and parsing. If the remote 
object is successfully replaced with a malformed or incompatible descriptor, 
silently serving the previous schema can make the decoder interpret new 
payloads with obsolete metadata instead of surfacing the bad deployment. 
Preserve the cached copy, but restrict fallback to genuine I/O failures; a 
fetched-but-invalid descriptor should fail the current initialization.



##########
pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufUtils.java:
##########
@@ -36,9 +41,25 @@ public class ProtoBufUtils {
   public static final String TMP_DIR_PREFIX = "pinot-protobuf";
   public static final String PB_OUTER_CLASS_SUFFIX = "OuterClass";
 
+  // Last successfully fetched (and parseable) content of each remote (S3, 
GCS, ...) descriptor file, keyed by URI.
+  // The descriptor is still fetched fresh on every decoder creation, so 
in-place updates of the file keep
+  // propagating exactly as before; this copy is served only when the fetch 
fails (e.g. a transient DNS or
+  // object-store outage), so a CONSUMING transition cannot go to ERROR on a 
network blip once the descriptor has
+  // been fetched once by this JVM. Bounded by total content size as a safety 
net.
+  private static final long FALLBACK_CACHE_MAX_WEIGHT_BYTES = 64L << 20;
+  private static final Cache<String, byte[]> LAST_KNOWN_GOOD_DESCRIPTORS = 
CacheBuilder.newBuilder()
+      .maximumWeight(FALLBACK_CACHE_MAX_WEIGHT_BYTES)
+      .weigher((String key, byte[] value) -> value.length)

Review Comment:
   **[Major] The 64 MiB limit does not bound total cache memory.** This weighs 
only `value.length`, excluding the URI string, entry/node overhead, and array 
overhead. More importantly, currently accepted empty descriptor sets have 
weight zero, so unique remote URIs can create an unbounded number of entries 
without consuming the configured weight. Please require semantically usable 
descriptors, charge a positive per-entry/key overhead, and/or add a maximum 
entry count.



##########
pinot-plugins/pinot-input-format/pinot-protobuf/src/main/java/org/apache/pinot/plugin/inputformat/protobuf/ProtoBufUtils.java:
##########
@@ -60,9 +81,53 @@ public static File getFileCopiedToLocal(String filePath)
     }
   }
 
+  /// Returns the content of the descriptor file at the given path. A remote 
file is fetched fresh on every call so
+  /// in-place updates are picked up, and the last successfully fetched (and 
parseable) content is remembered per
+  /// URI: when the fetch fails, or returns bytes that do not parse as a 
descriptor set, the remembered copy is
+  /// served instead so that decoder creation survives transient DNS / 
object-store outages. Local files are always
+  /// read fresh and never remembered.
+  ///
+  /// NOTE: Only descriptor files get this fallback. The jar used by 
[ProtoBufCodeGenMessageDecoder] is downloaded
+  /// via [#getFileCopiedToLocal(String)] without one (see the note there).
   public static InputStream getDescriptorFileInputStream(String 
descriptorFilePath)
       throws Exception {
-    return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    URI fileURI = URI.create(descriptorFilePath);
+    String scheme = fileURI.getScheme();
+    if (scheme == null || scheme.equals(PinotFSFactory.LOCAL_PINOT_FS_SCHEME)) 
{
+      return new FileInputStream(getFileCopiedToLocal(descriptorFilePath));
+    }
+    byte[] content;
+    try {
+      content = downloadFileToBytes(descriptorFilePath);
+      // Validate before remembering so that a corrupt/truncated download can 
neither be served nor overwrite the
+      // last known good copy
+      DynamicSchema.parseFrom(new ByteArrayInputStream(content));
+      LAST_KNOWN_GOOD_DESCRIPTORS.put(descriptorFilePath, content);
+    } catch (Exception e) {
+      content = LAST_KNOWN_GOOD_DESCRIPTORS.getIfPresent(descriptorFilePath);
+      if (content == null) {
+        throw e;
+      }
+      LOGGER.warn("Failed to fetch protocol buffer descriptor file: {}, 
falling back to the last successfully"
+          + " fetched copy", descriptorFilePath, e);
+    }
+    return new ByteArrayInputStream(content);
+  }
+
+  private static byte[] downloadFileToBytes(String filePath)
+      throws Exception {
+    File localFile = getFileCopiedToLocal(filePath);

Review Comment:
   **[Major] The fetch-failure path leaks its temporary directory.** 
`getFileCopiedToLocal()` creates the temp directory before calling 
`copyToLocalFile()`. When that copy throws, this method never receives a 
`File`, so execution cannot enter the `finally` below. I reproduced a leftover 
`pinot-protobuf*` directory after the simulated network failure—the exact path 
this fallback is intended to handle. Please keep temp creation, copy, and 
cleanup under one owner/finally, or read through `PinotFS.open(URI)`, and 
assert cleanup in the failure test.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to