Thrift: NPE on metadata-less empty FetchResults batch with hasMoreRows=true (should skip + refetch) (#1567) - #1589
Thrift: NPE on metadata-less empty FetchResults batch with hasMoreRows=true (should skip + refetch) (#1567)#1589peco-engineer-bot[bot] wants to merge 4 commits into
Conversation
The Engineer Bot's `Run author` step consumed its entire budget fighting the offline Maven setup rather than solving the issue. Two root causes, both reproduced from run 30479109185 (timed out at 45m after the agent had a correct fix at ~14m): 1. `surefire-junit-platform` was never cached — the warmup build runs `-DskipTests`, so the test phase (which lazily resolves the JUnit Platform provider) never executes. After creds are scrubbed and Maven goes offline, the agent's `mvn test` can't fetch it → ~25 min of dead retries. 2. Cached artifacts came back "present, but unavailable" — their `_remote.repositories` markers record `jfrog-central`, a repo id absent from the agent's empty offline settings. Fix (mirrors warmMavenCache.yml, which already solves both for forks): - Add a "Warm test dependencies" step that test-compiles, resolves plugins, and explicitly fetches surefire-junit-platform while JFrog creds are still live. - Normalize `_remote.repositories` (jfrog-central -> central) before the scrub so the warmed repo resolves cleanly offline. - Bump the job timeout 45 -> 60 min and add a 45-min step timeout on `Run author` so a stuck agent fails the step (letting the outcome comment report a real failure) instead of the job being force-cancelled. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
…ment Two review-bot findings on PR #1588, both fixed: - F1 (medium): the surefire-junit-platform resolution had no fallback, unlike the mirrored warmMavenCache.yml. Under `set -euo pipefail` a failed/empty `help:evaluate` substitution leaves SUREFIRE_VERSION empty, producing a malformed dependency:get coordinate silently swallowed by `|| true` — re-introducing the (absent) dead-end. Default to 3.1.2 when the expression can't be resolved. - F2 (low): the timeout comment's stale "~4 min" setup figure ignored the two new pre-author steps. Rewrote it to reflect the real setup+warmup cost (~10 min) and the 60-45=15 min non-author budget. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
Addresses review F1 (low, r3679000177): test-compile + the explicit surefire-junit-platform get resolve the test classpath and provider jar, but artifacts Surefire resolves LAZILY at execution time (JUnit-Platform engine internals, provider transitives) only land in ~/.m2 once a real JUnit-Platform run happens. Run the same fast test warmMavenCache.yml uses (DatabricksParameterMetaDataTest#testInitialization) while creds are live so the offline `mvn test` in Run author can't hit an execution-time resolution miss. `|| true` — warming the repo is the goal, not the verdict. Co-authored-by: Isaac Signed-off-by: eric-wang-1990 <e.wang@databricks.com>
…s=true (should skip + refetch) (#1567) Signed-off-by: peco-engineer-bot[bot] <3815206+peco-engineer-bot[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Verdict: 1 Medium · 1 Low
Targeted fix that loops FetchResults to skip metadata-less empty batches — correct in spirit and well-tested for the reported case. One medium concern: the new while (true) loop is unbounded and, unlike every other server-wait loop in this class, has no timeout guard, so a misbehaving server sequence hangs the thread forever. One low note on a residual NPE path when a metadata-less batch has hasMoreRows=false.
Other findings
- 🟡 Medium — The new
while (true)re-fetch loop has no iteration bound and no timeout guard. If the server keeps returning metadata-less empty batches withhasMoreRows=true(e.g. a buggy/looping backend, or an offset that never advances), the driver spins indefinitely, issuing FetchResults RPCs forever with no way to abort — the calling thread hangs and never surfaces an exception.
Every other loop in this class that waits on the server is bounded by a TimeoutHandler.checkTimeout() (see the polling loop at pollTillOperationFinished, line 341, and the metadata poll at line 794). This fetch loop is the one exception. Consider bounding the number of consecutive empty-batch skips, or wiring in the statement's query timeout via a TimeoutHandler, so a pathological server sequence fails cleanly instead of hanging.
- 🔵 Low — The skip condition is
metadata == null && hasMoreRows==true. A metadata-less batch withhasMoreRows==false(a final empty batch with no metadata) still falls through toreturn response, and the initial-fetch consumers described in the PR (ExecutionResultFactory.getResultSet/getResultHandler, theDatabricksResultSetThrift constructor) will then dereference the null metadata and throw the same NPE this PR is fixing. If the server can emit such a terminal metadata-less batch, this gap remains uncovered by both the fix and the two new tests. Worth confirming that case cannot occur, or handling it explicitly.
Summary
Automated fix for #1567 — Thrift: NPE on metadata-less empty FetchResults batch with hasMoreRows=true (should skip + refetch).
Fixed the NPE by making DatabricksThriftAccessor.executeFetchRequest (the shared initial-fetch path for both execute() and getStatementResult()) loop: when a FetchResults response has null result-set metadata but hasMoreRows=true, it is treated as a legitimate intermediate empty batch, skipped, and FetchResults is re-issued until metadata/rows arrive — matching the reference C#/ADBC drivers. Verified via the two provided tests (now green) plus the full 57-test DatabricksThriftAccessorTest class. The bug is only reproducible by injecting the crafted metadata-less empty-batch response (per the issue, done through the databricks-driver-test mitmproxy harness / RESULTFETCH-009/010 spec cases); a real warehouse does not emit this sequence on demand, so the deterministic mocked-client tests are the faithful reproduction.
Root cause & plan
Root cause: On the Thrift result path, the INITIAL TFetchResultsResp that seeds a DatabricksResultSet is assumed to always carry result-set metadata. When the server returns an empty row set with hasMoreRows=true and getResultSetMetadata()==null (a legitimate intermediate empty batch), the driver dereferences the null metadata rather than re-fetching, throwing NPE. Dereference sites reached from the initial response: ExecutionResultFactory.getResultSet (resultsResp.getResultSetMetadata().isSetIsStagingOperation()), ExecutionResultFactory.getResultHandler (getResultSetMetadata().getResultFormat()), and the DatabricksResultSet Thrift constructor (getResultSetMetadata().getResultFormat() in the switch, and passing metadata to DatabricksResultSetMetaData). Subsequent-batch loops (LazyThriftResult, AbstractRemoteChunkProvider, convertColumnarToRowBased, streaming) already tolerate empty batches because they only read getResults()/hasMoreRows, never metadata. The correct behavior per the issue and the C#/ADBC reference drivers is to skip a metadata-less empty batch and issue the next FetchResults until metadata arrives (or hasMoreRows=false). The fix belongs in DatabricksThriftAccessor where the initial fetch response is produced (the execute post-poll fetch branch and getStatementResult).
["jdbc-core/src/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.java", "jdbc-core/src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessorTest.java"]
Planned coverage:
Files changed
src/test/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessorTest.javasrc/main/java/com/databricks/jdbc/dbclient/impl/thrift/DatabricksThriftAccessor.javaTest plan
com.databricks.jdbc.dbclient.impl.thrift.DatabricksThriftAccessorTest#testExecute_skipsMetadataLessEmptyBatch— fails (red) against the original code, passes (green) after the fixcom.databricks.jdbc.dbclient.impl.thrift.DatabricksThriftAccessorTest#testExecute_skipsMultipleConsecutiveMetadataLessEmptyBatches— fails (red) against the original code, passes (green) after the fixNO_CHANGELOG=true
🤖 Generated by engineer-bot (bug-fix flow) — review before merge.