Skip to content

[1.3] Fix query partition memory retention and failure diagnostics - #19269

Merged
JackieTien97 merged 7 commits into
dev/1.3from
ty/fix-query-partition-memory
Nov 14, 2026
Merged

JackieTien97 merged 7 commits into
dev/1.3from
ty/fix-query-partition-memory

Conversation

@JackieTien97

@JackieTien97 JackieTien97 commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Description

Very large time-partition requests can keep query analysis running while retaining reservations from the operator memory pool. The Thrift buffer's 1.5x growth overflowed int above roughly 1.33 GiB, causing an entire large array to be copied for every small field write. The uncompressed frame limit was checked only after serialization, at flush().

The change makes these failures bounded and diagnosable:

  • Use overflow-safe buffer growth and write positions. Bound both the initial allocation and geometric growth by the applicable write-buffer limit. Writes fitting existing capacity copy directly; explicit limit checks run before growth.
  • Describe early rejection as a lower bound ("at least N bytes") and report the configured Thrift limit of the current transport.
  • Preserve the limit on the compressed wire frame, while validating compression/decompression buffer sizes before allocation and closing invalid transports.
  • Check the query deadline before and during time-partition enumeration.
  • Propagate invalid-frame errors from ConfigNode RPCs immediately instead of repeatedly serializing the same request and replacing the error with a connection failure. Transient transport failures still retry.
  • Report partition memory shortages as time-partition analysis failures with requested/available bytes. Stop applying stale result-column statistics to subsequent planning failures; keep the existing memory-quota status code.
  • Reject overflowing pending memory reservations atomically, preserving earlier reservations for cleanup.
  • Record an INFO context event containing the query ID and SQL when a failure escapes, including before registration. Callers retain responsibility for severity classification and stack traces, so semantic errors and timeouts are not escalated to WARN and exception stacks are not duplicated. Successful queries do not emit this context event.

Reproduction and validation

On the base revision 013622816a, a real 1,500,000,000-byte buffer grew by only 8 bytes for each of three successive writes, copying the entire array each time. With the fix, it grows once and subsequent writes reuse the buffer. This large-array check runs separately with a bounded 5 GiB heap; ordinary unit tests do not require multi-gigabyte allocations.

Regression tests exercise the real SQL parser, Coordinator, Analyzer, and generated ConfigNode Thrift serializer with test schema/partition fetchers and in-memory transports. Repeated oversized requests and partition-memory failures fully restore earlier operator-pool reservations. Tests verify INFO context records include the SQL and query ID for memory shortages, semantic errors, timeouts, and frame-size failures without an attached throwable, and that normal queries do not produce failure logs. Incremental 8-byte writes prove a 1 MiB cap never allocates beyond 1 MiB; initial capacities above the configured cap are also covered. Further cases exercise position overflow at the capacity fast path and oversized compressed output after reusing a buffer. A simulated OutOfMemoryError test verifies that SQL context is recorded and the original error is rethrown unchanged while earlier reservations are released.

86 distinct tests passed in a clean reactor build on JDK 21 (Java 17 target), covering normal and templated analysis, query retry, memory accounting, diagnostics, ConfigNode retry behavior, compressed/uncompressed frames, and buffer reuse/shrinking. Checkstyle and Spotless passed. The coverage job now explicitly selects JDK 21, matching Sonar, because the runner default JDK caused google-java-format to fail before any coverage tests could run.

A null-sink TBinaryProtocol microbenchmark on aarch64/JDK 21 compared 827763a31d with this change (32 MiB frame limit, 2 s warmup and 3 s measurement, median of 3 alternating JVM runs per variant):

Payload Before After
100k-slot TDataPartitionReq 13.674 ns/slot 10.594 ns/slot
1M-row TSInsertTabletReq 0.331 ns/row 0.319 ns/row

Small-field serialization time fell about 22.5%; large-binary results remained comparable. Payload lengths, hashes, and buffer capacities matched. These are local serialization measurements, not end-to-end query throughput.

mvn -B clean test -pl iotdb-core/datanode -am \
  -Dtest=AutoResizingBufferTest,TElasticFramedTransportTest,RpcUtilsTest,UrlUtilsTest,TSStatusCodeTest,QueryTimePartitionTest,QueryPartitionMemoryTest,AnalyzeTest,QueryExecutionRetryTest,MPPQueryContextTest,LocalExecutionPlannerOperatorsMemoryTest,ConfigNodeClientTest,TemplatedAnalyzeRetryTest \
  -Dsurefire.failIfNoSpecifiedTests=false -DfailIfNoTests=false

The local reproductions use controlled inputs and in-memory transports; they do not replay the original production SQL or start a live cluster.

  • Self-reviewed.
  • Added regression tests for failure handling and normal-query compatibility.
  • Added comments explaining the overflow, limits, diagnostics, and timeout behavior.

@JackieTien97 JackieTien97 changed the title [1.3] Fix query memory retention during oversized partition RPC serialization [1.3] Fix query partition memory retention and failure diagnostics Nov 13, 2026

@JackieTien97 JackieTien97 left a comment

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.

Reviewed at 856d4a0d82 (base 013622816a). I built base, 33277a6cf5, and this head and ran each as a 1C1D cluster (Windows, JDK 21, dn_thrift_max_frame_size=1048576, 64 devices with one point each). The inline numbers come from these clusters and from direct transport probes against the same jars.

Verified on this head

  • The oversized partition request (select s1 from root.db.** where time >= 0 and time < 3000000000000, ≈3.8 MB) now fails in 35 ms with Frame size (1048582) larger than protect max size (1048576)!. At 33277a6cf5 and on base it took 15.2 s (15 reconnect attempts) and reported "Fail to connect to any config node", even with a 3 s JDBC query timeout.
  • Compressed transport, two cases. At 33277a6cf5 both escaped as IllegalArgumentException, and the flush case left the transport open. Both now raise TTransportException and close the transport:
    • a maxCompressedLength overflow in flush() (forced 1.9 GB frame);
    • a varint length ≥ 2^31 in readFrame().
  • A near-unbounded range (time >= 0 and time <= 9223372036854775806) now fails with 1707 "Not enough memory while analyzing time partitions … requested this time 340.87 GB …, current free memory 772.99 MB".
  • The growth fix and the deadline checks look correct. startTime is only set by Coordinator, so standalone contexts are unaffected.
  • All 56 tests in the 7 touched test classes pass locally: ConfigNodeClientTest 2, MPPQueryContextTest 7, QueryPartitionMemoryTest 5, QueryTimePartitionTest 16, LocalExecutionPlannerOperatorsMemoryTest 12, AutoResizingBufferTest 9, TElasticFramedTransportTest 5.

Comments

  1. [P2] Coordinator: the new [QueryFailed] WARN logs every analysis/planning failure with a stack trace. That includes user errors that ErrorHandlingUtils keeps at INFO, and every other failure ends up logged twice.
  2. [P3] TElasticFramedTransport#write: buffer growth still overshoots the limit by 1.30–1.44×, and the error now reports the size at the crossing write instead of the request size.

Nit: sonar-codecov.yml. codecov has failed on the last five dev/1.3 pushes with the same spotless NoSuchMethodError (e.g. run 43390944744), so this JDK pin fixes the branch, not just this PR. It may deserve its own commit or PR so the fix survives if this PR is reworked.

Comment on lines +168 to +171
} catch (RuntimeException | Error e) {
// Analysis can fail before the execution is registered, so later error handling cannot look
// up its SQL in queryExecutionMap. Keep the SQL and query ID in the failure log here.
LOGGER.warn("[QueryFailed] queryId: {}, sql: {}", globalQueryId, sql, e);

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.

[P2] This logs every analysis/planning failure at WARN with a stack trace, including user errors that ErrorHandlingUtils deliberately keeps at INFO

ErrorHandlingUtils#onQueryException already logs these failures:

  • user-caused ones (SQL_PARSE_ERROR, SEMANTIC_ERROR, NO_PERMISSION, QUERY_TIMEOUT, …) at INFO with no stack trace;
  • everything else with the stack.

On the client path that line already includes the SQL: Query Statement: "<sql>". executeStatement failed because …. This catch runs before that classification. DataNode log on this head, client statements via JDBC:

statement existing ErrorHandlingUtils line new [QueryFailed] line
select count(s1), s1 from root.db.d0 → 701 INFO, no stack WARN + SemanticException stack (23 frames)
oversized partition request → 305 WARN + stack WARN + the same stack again
select s1 from root.db.d0 where time >= 0 and time <= 9223372036854775806 → 1707 WARN + stack WARN + the same stack again

The consequences:

  • A dashboard that keeps sending an invalid query now writes a WARN stack trace on every execution.
  • So does a query that hits the new analysis-time deadline check (QueryTimeoutRuntimeException).
  • On the client path, every other failure that throws out of execution() is logged twice.

If the goal is to tie the query ID to the SQL when a failure happens before registration, two options:

  • Log this line without the throwable, since callers already log it.
  • Skip the classes that ErrorHandlingUtils treats as user errors (at least SemanticException and QueryTimeoutRuntimeException), or log those at DEBUG.

assertFailureLogs in QueryPartitionMemoryTest would need to follow whichever option is chosen.

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.

Verified fixed at 827763a31d. I ran the same statements, and the DataNode log now shows:

statement ErrorHandlingUtils line [QueryFailed] line
select count(s1), s1 from root.db.d0 → 701 INFO, no stack INFO, no stack
oversized partition request → 305 WARN + stack (once) INFO, no stack
select s1 from root.db.d0 where time >= 0 and time <= 9223372036854775806 → 1707 WARN + stack (once) INFO, no stack

User errors no longer produce a WARN, and each failure's stack trace is logged only once.

close();
throw new TTransportException(
TTransportException.CORRUPTED_DATA,
"Frame size (" + size + ") larger than protect max size (" + maxSize + ")!");

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.

[P3] The reported size is now just past the limit, not the request size

For the same 64-device query:

  • base logged Frame size (3811712) larger than protect max size (1048576)!;
  • this head logs Frame size (1048582) larger than protect max size (1048576)!, which is also what the client now sees in the 305 message.

The old number showed how far over the limit the request was, which is what an operator needs in order to size dn_thrift_max_frame_size. The full size isn't known at this point anymore. Consider wording the message as "at least N bytes" and naming the setting.

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.

The new text fixes both points: it says "at least N" and it names a setting. However, TElasticFramedTransport is the shared service-rpc transport, so the hardcoded (dn_thrift_max_frame_size on DataNode) is wrong outside a DataNode:

  • Session/JDBC clients: the limit is the client's own Session.Builder#thriftMaxFrameSize (default 64 MiB, SessionConfig.DEFAULT_MAX_FRAME_SIZE) or JDBC's thrift_max_frame_size. The rejection happens on the client, before anything is sent.
  • ConfigNode: there is no setting at all. The limit is the fixed RpcUtils.THRIFT_FRAME_MAX_SIZE (536870912), because the ConfigNode never loads the DataNode config. For example, a ConfigNode whose iotdb-system.properties set dn_thrift_max_frame_size=1048576 still reported protect max size (536870912).

Repro: a plain Java client against this head's cluster (DataNode dn_thrift_max_frame_size=1048576), using new Session.Builder()...thriftMaxFrameSize(4194304) to insert a 1,000,000-row INT64 tablet:

base client jars:      IoTDBConnectionException: org.apache.thrift.transport.TTransportException: Frame size (16000109) larger than protect max size (4194304)!
this head's client:    IoTDBConnectionException: org.apache.thrift.transport.TTransportException: Serialized frame size is at least 8000077 bytes, exceeding the RPC frame-size limit (dn_thrift_max_frame_size on DataNode) of 4194304 bytes.

The message names a setting that didn't fire, and that setting's actual value here (1048576) doesn't match the number printed. A Session bulk insert over the 64 MiB default would point users to the server config, which can't fix a client-side rejection.

Suggestion: keep the library message generic, e.g. ... exceeding the configured Thrift max frame size of N bytes. If the DataNode hint is wanted, add it where the DataNode handles the failure (e.g. the CORRUPTED_DATA branch in ConfigNodeClient#executeRemoteCallWithRetry), or pass a limit description into the transport factory. The dn_thrift_max_frame_size assertions in TElasticFramedTransportTest and QueryPartitionMemoryTest would follow. My earlier suggestion to name the setting didn't account for the transport being shared.

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.

Verified fixed at c7003fff63. The message is now generic and names the limit that actually fired:

  • DataNode → ConfigNode, oversized partition request (305, 15–42 ms): Serialized frame size is at least 1048582 bytes, exceeding the configured Thrift max frame size of 1048576 bytes.
  • Plain Java Session client with thriftMaxFrameSize(4194304) against a DataNode set to 1048576: Serialized frame size is at least 8000077 bytes, exceeding the configured Thrift max frame size of 4194304 bytes. The number now matches the client's own limit.

The tests now also assert that dn_thrift_max_frame_size is absent from the message.

TTransportException.CORRUPTED_DATA,
"Frame size (" + size + ") larger than protect max size (" + maxSize + ")!");
}
writeBuffer.write(buf, off, len);

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.

[P3] Growth can still overshoot the limit before the rejection

writeBuffer.write grows the buffer by 1.5× without knowing getMaxWriteBufferSize(). The last growth step before the rejection can therefore allocate well past the limit. Measured on this head with 8-byte writes and a 1024-byte initial buffer:

limit capacity at rejection
1 MiB 1,513,342 (1.44×)
64 MiB (DataNode default for heaps ≥ 4 GiB) 87,266,902 (1.30×)

Passing the cap down to the write buffer would keep the array at or below the limit, e.g. min(grow, maxSize) when growing.

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.

Verified fixed at 827763a31d. Capacity at rejection is now exactly the limit (1,048,576 and 67,108,864, i.e. 1.00×), and testIncrementalGrowthDoesNotExceedFrameLimit covers it.

Optional perf note: these per-write checks sit on Thrift's hottest path. I benchmarked serialization into a null sink, taking the median of 3 alternating JVM runs.

  • Small fields: a 100k-slot TDataPartitionReq, i.e. 4 write calls per 12-byte struct.
  • Large binary: a 1M-row TSInsertTabletReq.
base this head this head + fast path below
small fields, ns/slot 8.87–9.03 9.97–12.10 8.03–8.69
large binary, ns/row 1.69–1.71 1.67–1.74 1.69–1.73

Only small-field-heavy messages pay the cost, about 11% or more; large binary writes are unaffected. This commit makes capacity <= limit an invariant, so a write that fits in the current array can skip every limit check. The checks are only needed when the buffer must grow:

// AutoScalingBufferWriteTransport
boolean tryWriteWithinCapacity(byte[] toWrite, int off, int len) {
  byte[] array = buf.array();
  if (len > array.length - pos) {
    return false;
  }
  System.arraycopy(toWrite, off, array, pos, len);
  pos += len;
  return true;
}

// TElasticFramedTransport#write, as the first statement
if (writeBuffer.tryWriteWithinCapacity(buf, off, len)) {
  return;
}

With only this change, the limit behavior in my probes is unchanged: a write is rejected after exactly limit bytes, capacity equals the limit, and the compressed paths behave as before. The "fast path" column above is this change.

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.

Verified at c7003fff63. The fast path doesn't change any limit behavior:

  • Writes are rejected after exactly limit bytes (1 MiB and 64 MiB), and capacity stays at 1.00× the limit.
  • Both compressed paths still raise TTransportException and close the transport.

Safety reasoning:

  • write(..., maxSize) is the only writer of the framed writeBuffer, so capacity ≤ limit holds and a fitting write can't cross the limit.
  • len > array.length - pos can't overflow.
  • testBufferedWriteCannotBypassArrayLimitOnOverflow would catch a pos + len rewrite.

Same benchmark as before (null sink, median ns over 3 alternating JVM runs):

base 827763a31d c7003fff63
small fields, ns/slot 8.80–9.02 9.89–10.29 8.37–8.50
large binary, ns/row 1.72–1.76 1.70–1.73 1.70–1.75

The small-field regression is gone; this head is now about 5% faster than base on that path. Large binary writes are unchanged.

@codecov

codecov Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.85057% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 47.48%. Comparing base (89dbf8f) to head (c7003ff).
⚠️ Report is 73 commits behind head on dev/1.3.

Files with missing lines Patch % Lines
...e/iotdb/db/queryengine/common/MPPQueryContext.java 93.75% 1 Missing ⚠️
Additional details and impacted files
@@              Coverage Diff              @@
##             dev/1.3   #19269      +/-   ##
=============================================
+ Coverage      45.56%   47.48%   +1.92%     
- Complexity       198      221      +23     
=============================================
  Files           3667     3678      +11     
  Lines         251340   252123     +783     
  Branches       31156    31360     +204     
=============================================
+ Hits          114529   119728    +5199     
+ Misses        136811   132395    -4416     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@JackieTien97 JackieTien97 left a comment

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.

Re-checked at 827763a31d. I rebuilt this head, ran the same 1C1D cluster (dn_thrift_max_frame_size=1048576, 64 devices) and transport probes, and the PR's CI is green (19/19).

All three earlier comments are fixed; details are in the thread replies. One follow-up and one optional note:

  • Error message: the new wording hardcodes the DataNode setting, but TElasticFramedTransport is the shared service-rpc transport. Session/JDBC clients and the ConfigNode get a message naming a setting that didn't trigger the rejection. Details in the reply on the message thread.
  • Optional perf note: the per-write checks cost about 11% when serializing small-field-heavy messages. Now that capacity never exceeds the limit, a fast path removes this cost. Details in the reply on the growth thread.

FYI, pre-existing and outside this PR: SessionConnection#callWithRetry retries every TException, including this deterministic CORRUPTED_DATA rejection. With the defaults (maxRetryCount=60, 500 ms interval), the Session probe in the message thread takes 30.7 s to fail. This PR doesn't touch SessionConnection, so base has the same loop. This is the same pattern this PR fixed in ConfigNodeClient, so it could be a follow-up.

Comment on lines +168 to +171
} catch (RuntimeException | Error e) {
// Analysis can fail before the execution is registered, so later error handling cannot look
// up its SQL in queryExecutionMap. Keep the SQL and query ID in the failure log here.
LOGGER.warn("[QueryFailed] queryId: {}, sql: {}", globalQueryId, sql, e);

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.

Verified fixed at 827763a31d. I ran the same statements, and the DataNode log now shows:

statement ErrorHandlingUtils line [QueryFailed] line
select count(s1), s1 from root.db.d0 → 701 INFO, no stack INFO, no stack
oversized partition request → 305 WARN + stack (once) INFO, no stack
select s1 from root.db.d0 where time >= 0 and time <= 9223372036854775806 → 1707 WARN + stack (once) INFO, no stack

User errors no longer produce a WARN, and each failure's stack trace is logged only once.

close();
throw new TTransportException(
TTransportException.CORRUPTED_DATA,
"Frame size (" + size + ") larger than protect max size (" + maxSize + ")!");

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.

The new text fixes both points: it says "at least N" and it names a setting. However, TElasticFramedTransport is the shared service-rpc transport, so the hardcoded (dn_thrift_max_frame_size on DataNode) is wrong outside a DataNode:

  • Session/JDBC clients: the limit is the client's own Session.Builder#thriftMaxFrameSize (default 64 MiB, SessionConfig.DEFAULT_MAX_FRAME_SIZE) or JDBC's thrift_max_frame_size. The rejection happens on the client, before anything is sent.
  • ConfigNode: there is no setting at all. The limit is the fixed RpcUtils.THRIFT_FRAME_MAX_SIZE (536870912), because the ConfigNode never loads the DataNode config. For example, a ConfigNode whose iotdb-system.properties set dn_thrift_max_frame_size=1048576 still reported protect max size (536870912).

Repro: a plain Java client against this head's cluster (DataNode dn_thrift_max_frame_size=1048576), using new Session.Builder()...thriftMaxFrameSize(4194304) to insert a 1,000,000-row INT64 tablet:

base client jars:      IoTDBConnectionException: org.apache.thrift.transport.TTransportException: Frame size (16000109) larger than protect max size (4194304)!
this head's client:    IoTDBConnectionException: org.apache.thrift.transport.TTransportException: Serialized frame size is at least 8000077 bytes, exceeding the RPC frame-size limit (dn_thrift_max_frame_size on DataNode) of 4194304 bytes.

The message names a setting that didn't fire, and that setting's actual value here (1048576) doesn't match the number printed. A Session bulk insert over the 64 MiB default would point users to the server config, which can't fix a client-side rejection.

Suggestion: keep the library message generic, e.g. ... exceeding the configured Thrift max frame size of N bytes. If the DataNode hint is wanted, add it where the DataNode handles the failure (e.g. the CORRUPTED_DATA branch in ConfigNodeClient#executeRemoteCallWithRetry), or pass a limit description into the transport factory. The dn_thrift_max_frame_size assertions in TElasticFramedTransportTest and QueryPartitionMemoryTest would follow. My earlier suggestion to name the setting didn't account for the transport being shared.

TTransportException.CORRUPTED_DATA,
"Frame size (" + size + ") larger than protect max size (" + maxSize + ")!");
}
writeBuffer.write(buf, off, len);

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.

Verified fixed at 827763a31d. Capacity at rejection is now exactly the limit (1,048,576 and 67,108,864, i.e. 1.00×), and testIncrementalGrowthDoesNotExceedFrameLimit covers it.

Optional perf note: these per-write checks sit on Thrift's hottest path. I benchmarked serialization into a null sink, taking the median of 3 alternating JVM runs.

  • Small fields: a 100k-slot TDataPartitionReq, i.e. 4 write calls per 12-byte struct.
  • Large binary: a 1M-row TSInsertTabletReq.
base this head this head + fast path below
small fields, ns/slot 8.87–9.03 9.97–12.10 8.03–8.69
large binary, ns/row 1.69–1.71 1.67–1.74 1.69–1.73

Only small-field-heavy messages pay the cost, about 11% or more; large binary writes are unaffected. This commit makes capacity <= limit an invariant, so a write that fits in the current array can skip every limit check. The checks are only needed when the buffer must grow:

// AutoScalingBufferWriteTransport
boolean tryWriteWithinCapacity(byte[] toWrite, int off, int len) {
  byte[] array = buf.array();
  if (len > array.length - pos) {
    return false;
  }
  System.arraycopy(toWrite, off, array, pos, len);
  pos += len;
  return true;
}

// TElasticFramedTransport#write, as the first statement
if (writeBuffer.tryWriteWithinCapacity(buf, off, len)) {
  return;
}

With only this change, the limit behavior in my probes is unchanged: a write is rejected after exactly limit bytes, capacity equals the limit, and the compressed paths behave as before. The "fast path" column above is this change.

@sonarqubecloud

Copy link
Copy Markdown

@JackieTien97 JackieTien97 left a comment

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.

Re-checked at c7003fff63 on a rebuilt 1C1D cluster (dn_thrift_max_frame_size=1048576) plus the same client and transport probes. Both round-2 follow-ups are addressed, and the earlier fixes still hold. Details are in the two thread replies. Nothing further from me.

Normal traffic also passed a round trip over the new fast path. A 50,000-row Session insertTablet read back via JDBC as count=50000, sum=1249975000, and all 50,000 rows fetched intact.

AutoResizingBufferTest (9) and TElasticFramedTransportTest (9) pass locally. CI was 18/19 green at posting time, with the auto-create-schema IT still running.

close();
throw new TTransportException(
TTransportException.CORRUPTED_DATA,
"Frame size (" + size + ") larger than protect max size (" + maxSize + ")!");

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.

Verified fixed at c7003fff63. The message is now generic and names the limit that actually fired:

  • DataNode → ConfigNode, oversized partition request (305, 15–42 ms): Serialized frame size is at least 1048582 bytes, exceeding the configured Thrift max frame size of 1048576 bytes.
  • Plain Java Session client with thriftMaxFrameSize(4194304) against a DataNode set to 1048576: Serialized frame size is at least 8000077 bytes, exceeding the configured Thrift max frame size of 4194304 bytes. The number now matches the client's own limit.

The tests now also assert that dn_thrift_max_frame_size is absent from the message.

TTransportException.CORRUPTED_DATA,
"Frame size (" + size + ") larger than protect max size (" + maxSize + ")!");
}
writeBuffer.write(buf, off, len);

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.

Verified at c7003fff63. The fast path doesn't change any limit behavior:

  • Writes are rejected after exactly limit bytes (1 MiB and 64 MiB), and capacity stays at 1.00× the limit.
  • Both compressed paths still raise TTransportException and close the transport.

Safety reasoning:

  • write(..., maxSize) is the only writer of the framed writeBuffer, so capacity ≤ limit holds and a fitting write can't cross the limit.
  • len > array.length - pos can't overflow.
  • testBufferedWriteCannotBypassArrayLimitOnOverflow would catch a pos + len rewrite.

Same benchmark as before (null sink, median ns over 3 alternating JVM runs):

base 827763a31d c7003fff63
small fields, ns/slot 8.80–9.02 9.89–10.29 8.37–8.50
large binary, ns/row 1.72–1.76 1.70–1.73 1.70–1.75

The small-field regression is gone; this head is now about 5% faster than base on that path. Large binary writes are unchanged.

@JackieTien97
JackieTien97 merged commit 7526403 into dev/1.3 Nov 14, 2026
19 checks passed
@JackieTien97
JackieTien97 deleted the ty/fix-query-partition-memory branch November 14, 2026 05:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant