Skip to content

HADOOP-19941. ABFS: Support Photon (Apache Arrow) ListBlobs on the Blob endpoint - #8611

Open
bhattmanish98 wants to merge 7 commits into
apache:trunkfrom
ABFSDriver:HADOOP-19941
Open

HADOOP-19941. ABFS: Support Photon (Apache Arrow) ListBlobs on the Blob endpoint#8611
bhattmanish98 wants to merge 7 commits into
apache:trunkfrom
ABFSDriver:HADOOP-19941

Conversation

@bhattmanish98

Copy link
Copy Markdown
Contributor

Description of PR

Adds config-gated support for consuming ListBlobs responses in the Apache Arrow (Photon) format on the ABFS Blob endpoint, with automatic, transparent fallback to the existing XML path and no public API changes.

JIRA: HADOOP-19941

Behaviour

  • New config fs.azure.photon.enabled (default false, opt-in). When enabled, ABFS advertises Arrow on ListBlobs via an Accept header (application/vnd.apache.arrow.stream, application/xml); the service may honour it or fall back to XML.
  • Response handling selects a parser by the response Content-Type — Arrow routes to the new Arrow parser, otherwise the existing XML SAX parser is used. Both produce the same BlobListResultSchema, so all downstream processing (rename-pending filtering, pagination, directory rectification) is unchanged.
  • Malformed Arrow responses surface as AbfsDriverException through the existing error-handling model rather than silently degrading.

Implementation

  • ResponseParserFactory chooses between XmlListBlobResponseParser and ArrowListBlobParser behind the ListBlobResponseParser interface.
  • ArrowListBlobParser reaches full parity with the XML parser: blob user metadata (Metadata map column), hdi_isfolder=true directory markers (case-insensitive; empty mkdir directories), implicit directories (BlobPrefix / ResourceType blobprefix|directory), copy properties, native TimeStampSec/UInt8 vectors, and continuation via schema NextMarker metadata.
  • Arrow/XML timestamps normalized to a single RFC 1123 GMT representation for identical FileStatus values; Arrow allocator memory limit is configurable.
  • Interrupt-safe parsing: reads the (already fully buffered) body through a non-interruptible channel instead of ArrowStreamReader's interruptible NIO channel, avoiding ClosedByInterruptException on interrupted threads — matching the XML path.

Telemetry

Adds Photon metrics: request count, response (Arrow served) count, fallback (XML returned) count, parse-failure count, and an end-to-end listing-latency duration tracker.

How was this patch tested?

  • Unit tests: parser selection, Arrow parsing (metadata/directory markers, blobprefix, native vector types, multi-page, special characters, allocator-limit, malformed streams, interrupt tolerance), request-header application, and metrics.
  • Integration tests: XML/Arrow parity, fallback, pagination, and metrics against a Blob-endpoint account, plus additions to the existing list-status ITest.
  • Documentation updated in blobEndpoint.md.

For code changes:

  • Does the title or this PR starts with the corresponding JIRA issue id (e.g. 'HADOOP-19941. Your PR title ...')?
  • Object storage: has the patch been tested against the target store (Azure Blob endpoint)?
  • If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under ASF? (adds org.apache.arrow:arrow-vector, Apache-2.0)

bhattmanish98 and others added 3 commits July 14, 2026 11:23
…oint

Add config-gated support for consuming ListBlobs responses in the Apache Arrow
(Photon) format on the ABFS Blob endpoint, with automatic, transparent fallback
to the existing XML path and no public API changes.

Behaviour
- New config fs.azure.photon.enabled (default false, opt-in). When enabled, ABFS
  advertises Arrow on ListBlobs via an Accept header
  (application/vnd.apache.arrow.stream, application/xml); the service may honour
  it or fall back to XML.
- Response handling selects a parser by the response Content-Type: an Arrow
  content type routes to the new Arrow parser, otherwise the existing XML SAX
  parser is used. Both produce the same BlobListResultSchema, so all downstream
  processing (rename-pending filtering, pagination, directory rectification) is
  unchanged.
- Malformed Arrow responses surface as AbfsDriverException through the existing
  error-handling model rather than silently degrading.

Parser
- ResponseParserFactory chooses between XmlListBlobResponseParser and
  ArrowListBlobParser behind the ListBlobResponseParser interface.
- ArrowListBlobParser reads the Arrow IPC stream and reaches full parity with
  the XML parser: blob user metadata (Metadata map column), hdi_isfolder=true
  directory markers (case-insensitive; empty mkdir directories), implicit
  directories (BlobPrefix rows / ResourceType blobprefix|directory), copy
  properties, native timestamp (TimeStampSec) and unsigned length (UInt8)
  vectors normalized to the XML representation, and continuation via the schema
  NextMarker custom metadata (empty normalized to null).
- The Arrow allocator memory limit is configurable, and Arrow/XML timestamps are
  normalized to a single RFC 1123 GMT representation so both paths yield
  identical FileStatus values and epochs.
- Parsing is immune to thread interrupts: instead of letting ArrowStreamReader
  wrap the (already fully buffered) body in an interruptible NIO channel - which
  would abort with ClosedByInterruptException when the caller's interrupt flag is
  set, e.g. task cancellation - it reads through a non-interruptible channel,
  matching the interrupt-tolerant XML SAX path.

Telemetry
- Adds Photon metrics: request count, response (Arrow served) count, fallback
  (XML returned) count, parse-failure count, and an end-to-end listing-latency
  duration tracker, wired through AbfsCountersImpl and emitted from listPath.

Tests
- Unit tests for parser selection (TestResponseParserFactory), Arrow parsing
  scenarios including metadata/directory markers, blobprefix, native vector
  types, multi-page, special characters, allocator-limit and malformed streams,
  and interrupt tolerance (TestArrowListBlobParser), Photon request-header
  application (TestAbfsBlobClientPhotonHeaders), and metrics (TestPhotonList
  BlobMetrics).
- Integration tests for XML/Arrow parity, fallback, pagination and metrics
  (ITestAbfsPhotonListStatus) plus additions to the existing list-status ITest.
- Documentation updated in blobEndpoint.md; Arrow INFO logs quieted in the test
  log4j configuration.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…tBlobs

Address code review feedback on the Photon (Apache Arrow) ListBlobs support:

* ArrowListBlobParser: cap the per-read heap staging buffer in the
  non-interruptible channel adapter at 8 KB instead of allocating a buffer
  sized to the reader's full request. ArrowStreamReader reads record-batch
  bodies through direct (off-heap) ByteBuffers, which take the staging path,
  so unbounded requests caused large transient allocations and GC pressure
  during listings. Arrow's readFully() loops on partial reads, so capping the
  chunk is safe and mirrors the JDK Channels.newChannel transfer-size cap.

* ResponseParserFactory.isArrowResponse(): match the negotiated Arrow IPC
  stream media type (application/vnd.apache.arrow.stream), tolerating
  parameters such as charset, instead of a loose substring check for "arrow".
  This prevents unrelated content types from being misrouted to the Arrow
  parser. Removed the now-unused CONTENT_TYPE_ARROW_TOKEN constant and added
  a regression test for content types that merely contain "arrow".

* pom.xml: remove the redundant --add-opens=java.base/java.nio=ALL-UNNAMED
  from the per-profile surefire argLine entries; it is already contributed by
  the shared ${maven-surefire-plugin.argLine} property.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…oton ListBlobs

* ITestAzureBlobFileSystemListStatus: make the list-parse failure assertion
  tolerant of the XML-fallback case. The wrapping error message is derived from
  the parser selected by the response Content-Type, not from the Photon config,
  so a Photon-enabled account that returns XML yields ERR_BLOB_LIST_PARSING.
  Assert the message contains either the Arrow or XML parsing failure string to
  avoid flakiness across accounts/service versions.

* DateTimeUtils: attach the caught DateTimeException as the log cause when an
  Arrow timestamp fails to parse, preserving the stack/details for debugging
  instead of only logging the raw value.

* ArrowListBlobParser: reuse a single fixed-size staging buffer per
  non-interruptible channel instance for direct-ByteBuffer reads instead of
  allocating a temporary array on every read, removing avoidable allocation/GC
  pressure during listing. Parsing drives the channel single-threaded, so the
  shared buffer is safe.

* AbfsHttpConstants: document that bumping ApiVersion.getCurrentVersion() to
  JUN_06_2026 is an intentional global default change required by the Photon
  (Arrow) ListBlobs contract, validated across all endpoint/operation
  combinations and therefore safe as the driver-wide default.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hadoop-yetus

Copy link
Copy Markdown

💔 -1 overall

Vote Subsystem Runtime Logfile Comment
+0 🆗 reexec 0m 53s Docker mode activated.
_ Prechecks _
+1 💚 dupname 0m 1s No case conflicting files found.
+0 🆗 codespell 0m 0s codespell was not available.
+0 🆗 detsecrets 0m 0s detect-secrets was not available.
+0 🆗 xmllint 0m 0s xmllint was not available.
+0 🆗 markdownlint 0m 0s markdownlint was not available.
+1 💚 @author 0m 0s The patch does not contain any @author tags.
+1 💚 test4tests 0m 0s The patch appears to include 7 new or modified test files.
_ trunk Compile Tests _
+1 💚 mvninstall 47m 19s trunk passed
+1 💚 compile 1m 1s trunk passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 compile 1m 2s trunk passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
+1 💚 checkstyle 0m 57s trunk passed
+1 💚 mvnsite 1m 6s trunk passed
+1 💚 javadoc 0m 58s trunk passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 javadoc 0m 56s trunk passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
+1 💚 spotbugs 1m 37s trunk passed
+1 💚 shadedclient 35m 5s branch has no errors when building and testing our client artifacts.
_ Patch Compile Tests _
+1 💚 mvninstall 0m 45s the patch passed
+1 💚 compile 0m 32s the patch passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 javac 0m 32s the patch passed
+1 💚 compile 0m 35s the patch passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
+1 💚 javac 0m 35s the patch passed
+1 💚 blanks 0m 0s The patch has no blanks issues.
-0 ⚠️ checkstyle 0m 27s /results-checkstyle-hadoop-tools_hadoop-azure.txt hadoop-tools/hadoop-azure: The patch generated 28 new + 8 unchanged - 0 fixed = 36 total (was 8)
+1 💚 mvnsite 0m 40s the patch passed
+1 💚 javadoc 0m 28s the patch passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 javadoc 0m 29s the patch passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
-1 ❌ spotbugs 1m 19s /new-spotbugs-hadoop-tools_hadoop-azure.html hadoop-tools/hadoop-azure generated 1 new + 0 unchanged - 0 fixed = 1 total (was 0)
+1 💚 shadedclient 33m 53s patch has no errors when building and testing our client artifacts.
_ Other Tests _
+1 💚 unit 2m 16s hadoop-azure in the patch passed.
+1 💚 asflicense 0m 35s The patch does not generate ASF License warnings.
135m 1s
Reason Tests
SpotBugs module:hadoop-tools/hadoop-azure
instanceof will always return true for all non-null values in org.apache.hadoop.fs.azurebfs.contracts.services.ArrowListBlobParser.readMetadata(MapVector, int), since all java.util.List are instances of java.util.List At ArrowListBlobParser.java:for all non-null values in org.apache.hadoop.fs.azurebfs.contracts.services.ArrowListBlobParser.readMetadata(MapVector, int), since all java.util.List are instances of java.util.List At ArrowListBlobParser.java:[line 405]
Subsystem Report/Notes
Docker ClientAPI=1.55 ServerAPI=1.55 base: https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-8611/1/artifact/out/Dockerfile
GITHUB PR #8611
Optional Tests dupname asflicense compile javac javadoc mvninstall mvnsite unit shadedclient codespell detsecrets xmllint spotbugs checkstyle markdownlint
uname Linux 5510ac8ff99c 5.15.0-181-generic #191-Ubuntu SMP Fri May 22 19:09:02 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
Build tool maven
Personality dev-support/bin/hadoop.sh
git revision trunk / 91fd5a8
Default Java Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
Multi-JDK versions /usr/lib/jvm/java-21-openjdk-amd64:Ubuntu-21.0.11+10-1-24.04.2-Ubuntu /usr/lib/jvm/java-17-openjdk-amd64:Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
Test Results https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-8611/1/testReport/
Max. process+thread count 585 (vs. ulimit of 10000)
modules C: hadoop-tools/hadoop-azure U: hadoop-tools/hadoop-azure
Console output https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-8611/1/console
versions git=2.43.0 maven=3.9.15 spotbugs=4.9.7
Powered by Apache Yetus 0.14.1 https://yetus.apache.org

This message was automatically generated.

- Remove redundant lastModifiedTime()/creationTime() accessors from
  BlobListResultEntrySchema; use lastModified()/creation() consistently.
- Fix BC_VACUOUS_INSTANCEOF in ArrowListBlobParser.readMetadata by using
  the typed List<?> returned by MapVector.getObject with a null check.
- Replace checkstyle MagicNumber literals with named constants in
  DateTimeUtils and the Photon Arrow tests.
- Anchor javac/java to the running JDK (java.home) in
  TestAggregateMetricsManager to avoid toolchain-version mismatch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@hadoop-yetus

Copy link
Copy Markdown

🎊 +1 overall

Vote Subsystem Runtime Logfile Comment
+0 🆗 reexec 19m 28s Docker mode activated.
_ Prechecks _
+1 💚 dupname 0m 1s No case conflicting files found.
+0 🆗 codespell 0m 0s codespell was not available.
+0 🆗 detsecrets 0m 0s detect-secrets was not available.
+0 🆗 xmllint 0m 0s xmllint was not available.
+0 🆗 markdownlint 0m 0s markdownlint was not available.
+1 💚 @author 0m 0s The patch does not contain any @author tags.
+1 💚 test4tests 0m 0s The patch appears to include 8 new or modified test files.
_ trunk Compile Tests _
+1 💚 mvninstall 47m 38s trunk passed
+1 💚 compile 1m 1s trunk passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 compile 1m 2s trunk passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
+1 💚 checkstyle 0m 56s trunk passed
+1 💚 mvnsite 1m 6s trunk passed
+1 💚 javadoc 1m 0s trunk passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 javadoc 0m 56s trunk passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
+1 💚 spotbugs 1m 36s trunk passed
+1 💚 shadedclient 35m 39s branch has no errors when building and testing our client artifacts.
_ Patch Compile Tests _
+1 💚 mvninstall 0m 47s the patch passed
+1 💚 compile 0m 33s the patch passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 javac 0m 33s the patch passed
+1 💚 compile 0m 35s the patch passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
+1 💚 javac 0m 35s the patch passed
+1 💚 blanks 0m 0s The patch has no blanks issues.
+1 💚 checkstyle 0m 26s the patch passed
+1 💚 mvnsite 0m 39s the patch passed
+1 💚 javadoc 0m 29s the patch passed with JDK Ubuntu-21.0.11+10-1-24.04.2-Ubuntu
+1 💚 javadoc 0m 29s the patch passed with JDK Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
+1 💚 spotbugs 1m 20s the patch passed
+1 💚 shadedclient 33m 54s patch has no errors when building and testing our client artifacts.
_ Other Tests _
+1 💚 unit 2m 16s hadoop-azure in the patch passed.
+1 💚 asflicense 0m 36s The patch does not generate ASF License warnings.
154m 29s
Subsystem Report/Notes
Docker ClientAPI=1.55 ServerAPI=1.55 base: https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-8611/2/artifact/out/Dockerfile
GITHUB PR #8611
Optional Tests dupname asflicense compile javac javadoc mvninstall mvnsite unit shadedclient codespell detsecrets xmllint spotbugs checkstyle markdownlint
uname Linux a01c1c7aa4c0 5.15.0-181-generic #191-Ubuntu SMP Fri May 22 19:09:02 UTC 2026 x86_64 x86_64 x86_64 GNU/Linux
Build tool maven
Personality dev-support/bin/hadoop.sh
git revision trunk / b4d0cdf
Default Java Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
Multi-JDK versions /usr/lib/jvm/java-21-openjdk-amd64:Ubuntu-21.0.11+10-1-24.04.2-Ubuntu /usr/lib/jvm/java-17-openjdk-amd64:Ubuntu-17.0.19+10-1-24.04.2-Ubuntu
Test Results https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-8611/2/testReport/
Max. process+thread count 568 (vs. ulimit of 10000)
modules C: hadoop-tools/hadoop-azure U: hadoop-tools/hadoop-azure
Console output https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-8611/2/console
versions git=2.43.0 maven=3.9.15 spotbugs=4.9.7
Powered by Apache Yetus 0.14.1 https://yetus.apache.org

This message was automatically generated.

*/
@VisibleForTesting
boolean applyPhotonRequestHeadersIfEnabled(
final List<AbfsHttpHeader> requestHeaders) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No HNS gating. Service returns 409 Conflict for Arrow listing on HNS accounts. A 409 never reaches ResponseParserFactory, so Content-Type fallback can't help — every listing fails hard.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Gated Photon on !getIsNamespaceEnabled() in applyPhotonRequestHeadersIfEnabled, so HNS accounts never advertise Arrow and stay on the XML path. Added testAcceptHeaderUnchangedOnHnsAccount and documented the restriction in blobEndpoint.md.

several java.base packages but not java.nio, so append it here to ensure
the default (no-profile) `mvn test` run can execute Arrow-based tests.
-->
<maven-surefire-plugin.argLine>-Xmx4096m -Xss4m -XX:+HeapDumpOnOutOfMemoryError ${extraJavaTestArgs} --add-opens=java.base/java.nio=ALL-UNNAMED</maven-surefire-plugin.argLine>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The --add-opens flag here only applies when running tests. Real clusters (Spark, Hive) won't have it, so Arrow will fail to initialise on Java 17 and listings will break — even though CI passes. Can we document the flag in blobEndpoint.md and fall back to XML if Arrow can't start?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented the --add-opens=java.base/java.nio=ALL-UNNAMED requirement for Java 17+ in blobEndpoint.md (driver JVM, e.g. Spark/Hive executors, not just tests), with guidance to keep fs.azure.photon.enabled=false when the flag can't be guaranteed. A true runtime "Arrow-init-failed → re-request XML" fallback isn't possible once the service has already returned an Arrow body, so the safe control is the config gate + the flag.

retryListOp.execute(tracingContext);
listResponseData = parseListPathResults(retryListOp.getResult(), uri);
listResponseData.setOp(retryListOp);
updatePhotonRequestMetric(photonRequested);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Metric invariant is broken on the rename-recovery retry path. updatePhotonRequestMetric() is called once before op.execute(), but the rename-recovery retry issues a second ListBlobs request whose response still goes through parseListPathResultsWithMetrics(). So PHOTON_RESPONSE_COUNT + PHOTON_FALLBACK_COUNT can exceed PHOTON_REQUEST_COUNT. Please increment the request counter for the retry call as well (or move the increment into a helper used by both calls).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right. The retry issues a second ListBlobs whose response is also classified, so I now call updatePhotonRequestMetric(photonRequested) before retryListOp.execute(...). RESPONSE + FALLBACK can no longer exceed REQUEST.

final AbfsRestOperation op, final URI uri, final boolean photonRequested)
throws AzureBlobFileSystemException {
final AbfsHttpOperation result = op.getResult();
updatePhotonResponseMetrics(photonRequested, result);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response/fallback classification also counts non-200 responses. This is invoked before the status code is checked, so an error response (404/409/etc., which carries an XML error body) increments PHOTON_FALLBACK_COUNT and makes the "service fell back to XML" signal unusable for rollout decisions. Classify only when result.getStatusCode() == HTTP_OK.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updatePhotonResponseMetrics now returns early unless result.getStatusCode() == HTTP_OK, so non-200 error bodies no longer inflate PHOTON_FALLBACK_COUNT.

if (!getAbfsConfiguration().isPhotonEnabled()) {
return false;
}
requestHeaders.removeIf(header -> ACCEPT.equals(header.getName()));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use equalsIgnoreCase

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the removeIf to ACCEPT.equalsIgnoreCase(header.getName()).

writer.start();
writer.writeBatch();
writer.end();
return out.toByteArray();

@anmolanmol1234 anmolanmol1234 Jul 28, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return out.toByteArray() is evaluated before writer.close(). It only works because end() flushes; please move ByteArrayOutputStream outside the try-with-resources and return after the block so the stream is probably complete

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved ByteArrayOutputStream outside the try-with-resources and return out.toByteArray() after the block, so the stream is guaranteed flushed/closed.

if (value == null) {
return null;
}
return DateTimeUtils.formatArrowDateTimeToRfc1123(value.toString());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc assumes getObject() yields an ISO-8601 local date-time. That's true for TimeStampSecVector / TimeStampMilliVector. But for the TZ variants (TimeStampSecTZVector, TimeStampMilliTZVector, TimeStampMicroTZVector), Arrow's getObject() returns a Long epoch value, not a LocalDateTime.

So value.toString() becomes "1783936279", which formatArrowDateTimeToRfc1123() won't recognize as ISO-8601 and will pass through unchanged. DateTimeUtils.parseLastModifiedTime() then fails on it, and every FileStatus gets a wrong or zero modification time — silently, with no exception and no metric.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

readTimestampAsRfc1123 now detects a Number (TZ vectors' epoch) and converts it via the column's ArrowType.Timestamp unit (sec/milli/micro/nano) → Instant → new DateTimeUtils.formatInstantToRfc1123. Fixed the Javadoc and added testTimeZoneTimestampNormalizedToRfc1123.

String value = readString((VarCharVector) vector, row);
if (value != null && !value.isEmpty()) {
String trimmed = value.trim();
return Boolean.parseBoolean(trimmed) || "1".equals(trimmed);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use constants for strings

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extracted NUMERIC_TRUE = "1".

}
Object value = vector.getObject(row);
if (value instanceof Number) {
return ((Number) value).longValue();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UInt8Vector.getObject() returns a Long holding the raw bit pattern, so an unsigned value above Long.MAX_VALUE comes back negative and is passed straight into entry.setContentLength(). The Long.parseLong text fallback below accepts "-1" just as readily. Could we reject negatives in setContentLength() (or use getObjectNoOverflow() for the UInt8Vector case), so a malformed response can't produce a FileStatus with a negative length?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setContentLength now rejects negatives (unsigned overflow / "-1") with a LOG.debug, leaving the default 0 rather than a negative FileStatus length. Added testNegativeContentLengthRejected.

try {
return Long.parseLong(text);
} catch (NumberFormatException ignored) {
// Leave unset if the value is not numeric.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A malformed length is swallowed here, so the entry falls back to 0 and surfaces as a zero-byte FileStatus — readers would treat the blob as empty rather than fail. Since a non-numeric length is a data-integrity signal rather than a benign absence, could we at least LOG.debug it, and confirm this matches what the XML path does with an unparseable ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added LOG.debug on the NumberFormatException in readLong. This matches the XML path, which also leaves an unparseable unset (→0). Added testNonNumericContentLengthIgnored.

}

while (reader.loadNextBatch()) {
BatchColumns columns = BatchColumns.resolve(root);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

varChar() returns null when a column is missing or present-but-not-a-VarCharVector, so if that happens for Name, buildEntry() returns null for every row and parse() completes normally with zero entries — no exception, and PHOTON_PARSE_FAILURE_COUNT never fires. Callers can't distinguish that from a genuinely empty directory, so the failure surfaces later as missing data in Spark/Hive rather than as a listing error. Could we throw here when columns.name == null? Name is the one column buildEntry() can't proceed without, so the "missing columns are tolerated" rule shouldn't extend to it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now throws IOException (with ERR_ARROW_LIST_PARSING) when columns.name == null. Updated testMissingMandatoryNameColumn to expect the throw and added testNullNameValueRowSkipped to confirm a present-but-null name value is still row-skipped.

}

while (reader.loadNextBatch()) {
BatchColumns columns = BatchColumns.resolve(root);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: resolve() allocates a HashMap and rescans every field vector on each batch, but ArrowStreamReader loads successive batches into the same VectorSchemaRoot and the same FieldVector instances, so the resolved references stay valid for the whole stream. Could we hoist this above the while loop? On a multi-batch listing this is per-batch overhead on the path whose whole purpose is to be faster than XML.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved once before the while loop (same VectorSchemaRoot/FieldVector instances persist across batches). Added testTwoBatchesInOneStream to exercise the multi-batch path.

|| metadata.isNull(row)) {
return result;
}
List<?> entries = metadata.getObject(row);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optimization: MapVector.getObject() materialises each row's entries as JsonStringHashMap instances, so a page with metadata allocates a map per key/value pair plus a boxed Text per string — on a 5000-blob listing that's the dominant allocation cost, and the instanceof Map / toString() unwrapping below then discards all of it. Given the perf motivation for this path, worth considering reading the underlying key/value VarCharVectors directly via the map's offset buffer instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — you're right that MapVector.getObject(row) materialises a JsonStringArrayList of JsonStringHashMaps plus boxed Text per key/value, and that the instanceof Map / toString() unwrapping then throws all of it away. I dug into the trade-off:

Pros of the direct key/value VarCharVector read

  • Eliminates the intermediate allocations on the metadata path: for R rows with M entries each, we drop the per-row list, the R×M JsonStringHashMaps, and the R×2M boxed Text objects.
  • Lower allocation rate → less young-gen GC churn → marginally better throughput on large, metadata-heavy listings.
  • No extra off-heap cost; we'd read straight from the existing map offset buffer + child key/value vectors.

Cons / limits

  • It doesn't reduce retained/peak heap. The data we actually keep — the returned Map<String,String> + key/value Strings per entry — is identical either way and lives until FileStatus conversion. So this is a transient-garbage optimization, not a footprint reduction. To shrink retained memory we'd have to stop materialising the full metadata map (e.g. extract only hdi_isfolder), which breaks XML parity.
  • The win scales with M. Typical blobs carry 0–2 user-metadata entries, so in the common case we're only skipping ~one tiny map per row — negligible. It's meaningful only when listings are both very large and metadata-rich.
  • Higher complexity/risk. It couples the parser to Arrow internals (MapVector → StructVector → child key/value VarCharVectors, per-row ranges via the offset buffer) with hand-rolled null/empty-map and multi-batch handling — precisely the kind of low-level code that's easy to get subtly wrong. And since we still need all keys for XML parity, we don't even get to short-circuit the traversal.

Proposal: keep the current readable getObject() version for this PR (correctness + parity), and track the offset-buffer read as a follow-up gated on a benchmark — if a profile of a large (≥50k), metadata-heavy listing shows readMetadata as a real hotspot/GC driver, I'll switch it and post the before/after numbers. Happy to file the JIRA and link it here. WDYT?

* @return an absolute path to the tool inside the running JDK, or {@code name}.
*/
private static String jdkTool(String name) {
String javaHome = System.getProperty("java.home");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use constants

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added JAVA_HOME_PROPERTY and JDK_BIN_DIR constants.

private static String jdkTool(String name) {
String javaHome = System.getProperty("java.home");
if (javaHome != null && !javaHome.isEmpty()) {
File tool = new File(new File(javaHome, "bin"), name);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here as well

* Unit tests for {@link ArrowListBlobParser}, the Photon (Apache Arrow based)
* ListBlobs response parser.
*/
public class TestArrowListBlobParser {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Few tests which can be added

  1. Timestamps with a timezone. All the tests use TimeStampSecVector, which has no zone. The TZ variants (TimeStampSecTZVector, TimeStampMilliTZVector) hand back a plain epoch number instead of a date object, so readTimestampAsRfc1123() would turn it into a string like "1783936279" and pass it straight through. Every file would end up with the wrong modified time and nothing would fail. The service could switch to a TZ column without that being a breaking change, so worth a test.

  2. Two batches in one stream. testMultiPageListingHandledCorrectly() parses two separate streams, each with one batch, so the while (loadNextBatch()) loop never actually loops and the per-batch column lookup is never re-run.

  3. Smaller ones: a UInt8 length big enough to go negative, a length that isn't a number, hdi_isfolder = "1", CopyCompletionTime sent as a real timestamp vector rather than a string, and a name that's just "/".

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added: TZ timestamp, two batches in one stream, negative UInt8 length, non-numeric length, hdi_isfolder="1" column, and a name of just "/".

bhattmanish98 and others added 2 commits August 6, 2026 01:49
Blob endpoint / Photon (Apache Arrow) ListBlobs review fixes:

- AbfsBlobClient: gate Photon on non-HNS accounts (the Blob endpoint rejects
  an Arrow ListBlobs request on HNS with 409, which the XML fallback cannot
  recover), use equalsIgnoreCase for the ACCEPT header removal, count the
  rename-recovery retry as a Photon request so the request/response/fallback
  metric invariant holds, and classify the response/fallback metric only on
  HTTP 200.
- ArrowListBlobParser: fail loudly when the mandatory Name column is absent,
  resolve batch columns once outside the batch loop, handle timezone-aware
  Arrow timestamp vectors (epoch values) when normalizing to RFC 1123, reject
  negative (unsigned-overflow) content lengths, log malformed length values,
  and use a named constant for the numeric-true value.
- DateTimeUtils: add formatInstantToRfc1123 for the epoch timestamp path.
- blobEndpoint.md: document the non-HNS restriction and the Java 17+
  --add-opens=java.base/java.nio requirement for Arrow.
- Tests: fix ByteArrayOutputStream scoping, use constants for JDK tool
  resolution, and add Arrow parser coverage (TZ timestamp, two batches in one
  stream, negative and non-numeric length, hdi_isfolder="1", name "/").

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants