8000 fix(memory): bound the data path in BYTES, not only in rows -- handoff byte cap, Debezium queue byte bound, byte-bounded INSERT chunks, bounded legacy queue (Spec 01.05 section 3.4 items 7-8, Spec 03.06 section 3.1) by minguyen9988 · Pull Request #1505 · Altinity/clickhouse-sink-connector · GitHub
[go: up one dir, main page]

Skip to content

fix(memory): bound the data path in BYTES, not only in rows -- handoff byte cap, Debezium queue byte bound, byte-bounded INSERT chunks, bounded legacy queue (Spec 01.05 section 3.4 items 7-8, Spec 03.06 section 3.1) - #1505

Merged
minguyen9988 merged 5 commits into
2.11.0from
fix-handoff-byte-bounds
Sep 26, 2026
Merged

minguyen9988 merged 5 commits into
2.11.0from
fix-handoff-byte-bounds

Conversation

@minguyen9988
Copy link
Copy Markdown
Collaborator

Stacked on #1504 (its two commits are included here; the diff shrinks to the last two commits once #1504 merges).

Defect

Every bound on the data path was expressed in rows or in batches, none in bytes, on a JVM with a fixed heap and a source that can burst far faster than the sink writes:

  1. Debezium's own change-event queue: max.queue.size (8192 events) only; max.queue.size.in.bytes defaults to 0 = off and was never set by the connector or its templates. 8192 events of a narrow table is ~160 MB, of a table with megabyte BLOB/JSON rows ~20 GB -- in front of every bound the sink applies.
  2. The handoff hard cap (fix(cdc): hard cap on rows handed off and not yet acknowledged -- the reader pauses at the cap instead of filling the heap until the JVM is lost to garbage collection (Spec 01.05 section 3.4) #1503) is in rows: 500,000 rows of a narrow table is ~10 GB of retained heap, 500,000 rows of megabyte rows is an order of magnitude past any heap, so the JVM is lost to garbage collection before the cap can pause the reader -- the very failure the cap exists to prevent.
  3. The JDBC v2 driver renders each INSERT chunk as SQL text in memory (the substituted statement per addBatch, then one concatenated INSERT) before sending it; buffer.max.records bounds the chunk in rows only, once per worker thread.
  4. The single-threaded queue was constructed unbounded when sink.connector.max.queue.size was absent: setup() read the raw Properties, so the ConfigDef default never reached it.

Rule (spec 01.05 section 3.4 items 7-8, section 3.6; spec 03.06 section 3.1)

  • RecordSizeEstimator: a deterministic, JVM-independent estimate of a row's retained heap -- the Debezium envelope (key + value) walked field by field, times a retention factor of 3, plus 512 bytes per row -- sampled ONCE per group (the rows of one table in one batch) and stamped on every row (ClickHouseStruct.estimatedBytes).
  • DebeziumOffsetManagement keeps outstandingBytes beside outstandingRecords, added at handoff and released at acknowledgement (a parked unit still counted, reset() zeroes it). awaitHandoffCapacity(rows, bytes, timeout, liveness) pauses the reader when EITHER cap is met; sink.connector.handoff.max.outstanding.bytes defaults to one quarter of the maximum heap (floor 256 MiB), 0 disables the byte dimension. The 3-argument overload is kept. Log lines carry the estimated MiB beside the row counts.
  • DebeziumQueueBytesPreflight sets max.queue.size.in.bytes to one sixteenth of the maximum heap (floor 64 MiB) when the operator set nothing; an explicit value, including 0, is kept and logged.
  • BatchChunker replaces Lists.partition in PreparedStatementExecutor: chunks close on buffer.max.records OR buffer.max.bytes (default 256 MiB of estimated bytes); order preserved, a row never split, an oversized row written on its own.
  • The legacy queue is bounded by ClickHouseSinkConnectorConfig.DEFAULT_MAX_QUEUE_SIZE when the property is absent.

Checks on this branch

  • validate_specs: PASSED (63 specifications, 650 citations).
  • sink-connector: 62/62 -- new RecordSizeEstimatorTest 4/4, HandoffHardCapBytesTest 5/5, BatchChunkerTest 5/5; unchanged HandoffHardCapBackpressureTest 7/7, HandoffHardCapLogPacingTest 4/4, HandoffBacklogAdvisoryTest 4/4, DebeziumOffsetManagementTest 4/4, RetiredHandoffNotAcknowledgedTest 4/4, OffsetNoLossParameterIndependenceTest 5/5, PreparedStatementExecutor*Test all green. BUILD SUCCESS.
  • sink-connector-lightweight: 23/23 -- new DebeziumQueueBytesPreflightTest 4/4; BinlogKeepAlivePreflightTest 7/7, StoppedEngineRetiresHandoffsTest 3/3, DeadWorkerRetryIsTerminalTest 3/3, EngineRestartFifoResetTest 6/6. BUILD SUCCESS.

…cillates one unit at a time, so one WARN/INFO per wait was one per batch (~300 lines a minute, 990 WARNs into the error log in seven minutes on the first deployment that met the cap); now one WARN per pacing period, pauses inside a 60 s re-arm window counted not logged, one summary INFO per minute while paced, one 'pacing ended' line after a quiet gap or on reset() (Spec 01.05 section 3.4 item 6)
…lands 52 s into the period -- at 61 s the per-minute summary fires by design, which the test had mistaken for a stray line
… cap on the handoff (sink.connector.handoff.max.outstanding.bytes, default a quarter of the heap, estimated per group by RecordSizeEstimator and stamped on every row), a connector-pinned default for Debezium's max.queue.size.in.bytes (a sixteenth of the heap; Debezium's own default is 0 = off), INSERT chunks closed on estimated bytes as well as rows (buffer.max.bytes, default 256 MiB), and the single-threaded queue bounded by the ConfigDef default when the property is absent (Spec 01.05 section 3.4 items 7-8, section 3.6; Spec 03.06 section 3.1)
…g body -- checkIfBatchCanBeCommitted declares InterruptedException
… one squashed commit with the same content as the two pacing commits this branch was stacked on; the three conflicts (DebeziumOffsetManagement.java, spec 01.05, spec 09.01) resolve to this branch's versions, which carry the pacing text plus the byte-bound additions
@minguyen9988
Copy link
Copy Markdown
Collaborator Author

Conflict resolved: #1504 landed on 2.11.0 as one squashed commit (5120dc5) whose content is byte-identical to the two pacing commits this branch was stacked on (git diff between them is empty), so the three conflicts -- DebeziumOffsetManagement.java, spec 01.05, spec 09.01 -- resolve to this branch's versions, which carry the pacing text plus the byte-bound additions. Merged 5120dc5 into the branch as a merge commit (2212fdd, fast-forward push, no rebase); the diff against 2.11.0 is now exactly the byte-bound change (16 files). Re-ran on the merge result: validate_specs PASSED (63 specs, 650 citations); sink-connector 62/62 (RecordSizeEstimatorTest 4/4, HandoffHardCapBytesTest 5/5, BatchChunkerTest 5/5, HandoffHardCapBackpressureTest 7/7, HandoffHardCapLogPacingTest 4/4, HandoffBacklogAdvisoryTest 4/4, DebeziumOffsetManagementTest 4/4, RetiredHandoffNotAcknowledgedTest 4/4, OffsetNoLossParameterIndependenceTest 5/5, PreparedStatementExecutor*Test green); sink-connector-lightweight 23/23 (DebeziumQueueBytesPreflightTest 4/4, BinlogKeepAlivePreflightTest 7/7, StoppedEngineRetiresHandoffsTest 3/3, DeadWorkerRetryIsTerminalTest 3/3, EngineRestartFifoResetTest 6/6). BUILD SUCCESS in both modules.

@minguyen9988
minguyen9988 merged commit eee81c1 into 2.11.0 Sep 26, 2026
2 checks passed
@minguyen9988
minguyen9988 deleted the fix-handoff-byte-bounds branch September 26, 2026 06:25
minguyen9988 added a commit that referenced this pull request Sep 26, 2026
…onflict was the setup() preflight block, where #1505 added DebeziumQueueBytesPreflight next to the ResumeReplayLogSummary install; both are kept, queue preflight first
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

0