Repository navigation
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #19108 +/- ##
============================================
+ Coverage 48.43% 48.89% +0.46%
- Complexity 635 689 +54
============================================
Files 5572 5588 +16
Lines 412012 413448 +1436
Branches 54309 54538 +229
============================================
+ Hits 199556 202171 +2615
+ Misses 212456 211277 -1179 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
The failing SonarCloud quality gate (new_reliability_rating = 3) is not caused by this change: it comes from two bugs in recently merged pipe code and currently fails for every pull request. #19109 fixes them (its own SonarCloud gate passes); once it is merged, the gate here should pass on the next analysis. |
|
@jt2594838 @HTHou plz take a look |
A flush reserves memSize / seriesNumber * io_task_queue_size_for_flushing and takes it out of the MemTable budget. With fewer series than queue slots (default 10) the estimate exceeds the memtable itself: a memtable with 3 series reserved 3.3 times its size, more than the whole MemTable budget, so the reject threshold became negative and writes were rejected. The bounded IO queue can never hold more than the memtable, so the estimate is now capped at memSize when there are no more series than queue slots. With more series the estimate is unchanged.
20b3655 to
506a770
Compare
| if (!encodingFasterThanIo || seriesNumber == 0) { | ||
| return 0; | ||
| } | ||
| return seriesNumber <= ioTaskQueueSize ? memSize : memSize / seriesNumber * ioTaskQueueSize; |
There was a problem hiding this comment.
Could we leave some headroom for the encoded chunk buffers here? The I/O queue holds chunk writers, and a single series can enqueue multiple chunks. The writers' backing arrays can also grow larger than the raw data counted by memSize().
I reproduced this by pausing I/O until the queue filled, using one device with nine DOUBLE series (PLAIN, UNCOMPRESSED, 60,000 points per series). The queued writers' backing arrays alone occupied 9,406,224 bytes on both revisions. The base reserved 9,600,000 bytes, while this PR reserved 8,640,000 bytes. That leaves 766,224 bytes unaccounted for when computing the write rejection threshold.
Can we keep the reduction in over-reservation while accounting for those buffers, and add a regression test that exercises the reservation with a stalled I/O consumer?
There was a problem hiding this comment.
Thanks, good catch. I could reproduce it: with I/O stalled until the queue is full, one device with nine DOUBLE series (PLAIN, UNCOMPRESSED, 60,000 points each) gives 9,441,072 bytes of queued chunk buffers (ChunkWriterImpl.pageBuffer) against a memSize() of 8,640,000. I only got numbers close to yours with the time column PLAIN as well; with the default TS_2DIFF time encoder the buffers stay at about half of the reservation.
The data written into the buffers matches memSize() almost exactly (8,649,045 bytes). The difference is the spare capacity the buffers keep after growing by doubling. The old estimate doesn't cover it either. It only came out ahead in this case because ten queue slots for nine series add about 11%. With more series than slots, both versions reserve the same amount and both come up short:
| Series × points | Reservation: master / this PR so far | Queued buffers | Buffers / reservation |
|---|---|---|---|
| 9 × 60,000 | 9,600,000 / 8,640,000 | 9,441,072 | 0.98 / 1.09 |
| 30 × 60,000 | 9,600,000 (both) | 10,490,080 | 1.09 |
| 300 × 6,000 | 960,000 (both) | 1,311,540 | 1.37 |
Since the buffers' capacity can approach twice their content, the estimate now doubles the queued share. The cap on the raw share stays, so the reservation is at most twice the memtable, instead of up to ten times (the queue size divided by the number of series) before this PR.
MemTableFlushTaskTest.testReservationCoversQueuedChunkBuffersWithStalledIo is the regression test you asked for. It stalls the I/O consumer, waits until the encoder has filled the queue, and checks that the reservation actually taken from SystemInfo covers the capacity of the queued chunk buffers, for 9 × 60,000, 1 × 60,000 and 300 × 6,000 (time and values PLAIN). It fails without the factor (9,441,072 B of buffers against 8,640,000 B reserved) and passes with it.
One trade-off: the flushing memtable keeps counting towards the reject threshold until the flush ends, and the reservation lowers that threshold. In small memory configurations with few series per memtable, a 2× reservation can therefore put writes into reject status for the duration of a flush. If you think that matters, I could make the factor configurable (e.g. temporary_memory_factor_for_flushing, default 2.0), so such setups can lower it. I left it out to keep this PR small.
…buffers The queued chunk writers' page buffers grow by doubling, so their capacity can exceed the raw data counted by memSize. With the I/O consumer stalled, nine PLAIN DOUBLE series of 60,000 points (time column PLAIN as well) queue 9,441,072 bytes of buffers against a memSize of 8,640,000. The previous estimate falls short the same way once there are more series than queue slots (300 series of 6,000 points: 1.37 times the reservation). The estimate now doubles the queued share, which is still capped at the memtable size, so a flush reserves at most twice the memtable. MemTableFlushTaskTest stalls the I/O consumer until the queue is full and checks that the reservation covers the capacity of the queued buffers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JackieTien97
left a comment
There was a problem hiding this comment.
Found one remaining under-reservation case in the updated flush memory estimate; reproduction details are inline.
| long queuedShare = | ||
| seriesNumber <= ioTaskQueueSize ? memSize : memSize / seriesNumber * ioTaskQueueSize; | ||
| return queuedShare * CHUNK_BUFFER_GROWTH_FACTOR; |
There was a problem hiding this comment.
[P2] Account for encoding expansion before applying the buffer growth factor
The 2× growth bound applies to the encoded byte count, but memSize() counts raw values. For example, PLAIN-encoded INT32 values can occupy 5 bytes each while memSize() counts only 4, so doubling the raw share can still under-reserve the queued buffers.
I reproduced this on d458e7be55 using the full MemTableFlushTask pipeline with I/O stalled at startChunkGroup: one device, one non-aligned INT32 series, 41,000 rows, timestamps i, values Integer.MAX_VALUE - i, PLAIN time/value encoders, and UNCOMPRESSED data. With the default queue/chunk settings, the queue contains one chunk writer:
| Measurement | Bytes |
|---|---|
memSize() |
492,000 |
Retained buffers, measured with this PR's retainedArrayBytes helper |
1,048,912 |
| Actual reservation with this PR | 984,000 |
| Actual reservation with the base flush implementation | 4,920,000 |
The same buffer-coverage assertion fails with the PR implementation and passes with the base implementation. This leaves at least 64,912 bytes unaccounted for when reducing the MemTable budget and computing the write-rejection threshold. The existing 18 targeted tests pass, but the new stalled-I/O cases only exercise DOUBLE values.
Could we account for encoding expansion and page overhead before applying the capacity-growth allowance, and add this INT32 case to the stalled-I/O regression test?
There was a problem hiding this comment.
Thanks, confirmed: with your INT32 case I get the same 1,048,912 bytes of queued buffers against a reservation of 984,000.
To see how far this goes, I measured the encoded size against memSize() for every data type and encoding with random full-range values (PLAIN time column). Variable-length integers are the worst case: about 1.08 for PLAIN and ZIGZAG INT32 and 1.09 for ZIGZAG INT64, with a bound of 1.25 for the values themselves (5 bytes for an int, 10 for a long). All other encodings stay within about 6% (RLBE is the highest), and TEXT and BOOLEAN with RLE stay below memSize(). Page headers are included in these numbers.
Since the buffers' capacity can reach twice their content, the estimate now multiplies the queued share by 2.5 (2 × 1.25) instead of 2, still capped at the memtable size (de0924b). Your INT32 case (one series, 41,000 rows, Integer.MAX_VALUE - i, PLAIN, uncompressed) is now part of testReservationCoversQueuedChunkBuffersWithStalledIo; it fails with the factor 2 and passes with 2.5.
I also considered a per-series bound from the encoders' maximum item sizes, which would be tighter, but it means walking the memtable's schemas for the estimate. I kept the constant for now; happy to switch if you prefer that.
…nd buffer growth memSize counts raw values, but the queued chunk buffers hold encoded data, which variable-length integers make up to 1.25 times larger (5 bytes for a PLAIN INT32, 10 for a ZIGZAG INT64); the other encodings measured stay within about 6%. On top of that, the buffers' capacity can reach twice their content. One PLAIN INT32 series of 41,000 large values queues 1,048,912 bytes of buffers against a memSize of 492,000 and a 2x reservation of 984,000. The estimate now multiplies the queued share by 2.5 (2 x 1.25), still capped at the memtable size. The stalled-I/O test covers this INT32 case; it fails with the factor 2 and passes with 2.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|




Description
MemTableFlushTask.syncFlushMemTable()reserves temporary memory for a flush and takes it out ofthe MemTable budget (
SystemInfo.reserveTemporaryMemoryForFlushing, since #18848; before thatapplyTemporaryMemoryForFlushingwith the same estimate):The bounded IO queue holds up to
io_task_queue_size_for_flushing(default 10) encoded series ofaverage size. With fewer series than queue slots the formula estimates more than the memtable
itself: a memtable with 3 series reserves 10/3 = 3.3 times its size. With a small MemTable budget
a single flush can then reserve more than the whole budget, so
FLUSH_THRESHOLDandREJECT_THRESHOLDbecome negative and the node rejects writes until the flush is done.Measured with the same code in a downstream fork (standalone DataNode, 147.9 MB MemTable budget),
writing to one device with 3 series (flushes of about 52 MB):
The queue can never hold more than the memtable, so the estimate is now capped at
memSizewhen there are no more series than queue slots; with more series it is unchanged.
The estimate is moved into a small static method
estimateTemporaryMemory(...)so that it canbe unit tested (
FlushMemoryEstimateTest).Tests
FlushMemoryEstimateTest(fails before the change: 173.3 MB estimated for a 52 MB memtablewith 3 series).
MemTableFlushTaskTest,TsFileProcessorTest,SystemInfoTest,DataRegionTest: 106 testspass;
spotless:checkpasses.🤖 Generated with Claude Code