Summary
A live view writes a head checkpoint only on a refresh cycle that emitted rows. A view whose filter rejects every row of a stretch of base ingest keeps consuming the commits, but its head checkpoint stays where the last emitted row left it, and WalPurgeJob holds the base table's WAL at that checkpoint.
The base table's WAL segments and sequencer parts then accumulate for as long as no matching row arrives. cairo.live.view.checkpoint.max.duration.micros does not bound it, and live_views() shows nothing unusual: the view is active with zero lag.
To reproduce
CREATE TABLE base (ts TIMESTAMP, sym SYMBOL, x DOUBLE) TIMESTAMP(ts) PARTITION BY DAY WAL;
CREATE LIVE VIEW lv FLUSH EVERY 100ms START FROM BEGINNING AS
SELECT ts, sym, x, sum(x) OVER (PARTITION BY sym ORDER BY ts ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) s
FROM base
WHERE x > 0;
- Commit a few rows the filter accepts (
x = 1.5), in timestamp order.
- Keep committing rows it rejects (
x <= 0), in timestamp order.
- Count the
wal*/<segment> directories of base, and compare with a table that has an unfiltered view or no view.
These settings make it visible within seconds. They only change the measurement granularity; the default-settings run below shows the same retention.
cairo.wal.segment.rollover.row.count=1
cairo.wal.purge.interval=300
cairo.live.view.checkpoint.max.duration.micros=1000000
Observed
On a full ServerMain at 6c63556431, on the wall clock, with the three settings above: 6 accepted commits, then rejected commits every 250 ms for 15 s (15 duration cadences), then 4 s without commits.
| table |
view |
commits |
WAL segment directories kept |
head checkpoint base seqTxn |
base |
filtered lv |
62 |
61 |
1 |
ctl |
unfiltered view |
62 |
6 |
60 |
plain |
none |
62 |
1 |
n/a |
live_views() for lv: view_status = active, lag_seqtxn = 0, lv_consumed_seqtxn = 62, checkpoint_timeline_normalized_base_seqtxn = 1.
- A server restart changes nothing:
base still keeps 61 segments.
- One accepted row moves the head checkpoint to the latest commit, but the purge floor stays at 1 and
base keeps 62 segments. The floor is the minimum over both checkpoint timeline slots, so the WAL is released only after a second seal.
DROP LIVE VIEW releases everything, and so does an invalidation of the view.
With the default segment rollover (200,000 rows) and 300 commits of 5,000 rejected rows each, base keeps 8 segments (42.0 MB) where the control table keeps 2 (8.4 MB), measured after a restart. That is all base ingest since the pinned commit.
The duration setting was also tried at 1 s with cairo.live.view.checkpoint.rows=1 on a hand-driven job: the clock advanced 10 s per commit and then 3 idle hours, and the head checkpoint did not move.
Cause
A cycle that emits nothing advances the consumed watermark and returns without sealing. Its comment states an intent that the missing seal defeats: "Advance the watermarks anyway so base WAL retention releases the consumed segments."
|
if (flushRows == 0 || tier == null) { |
|
// Nothing to materialise (only non-data / filtered base commits walked, |
|
// or no tier). Advance the watermarks anyway so base WAL retention |
|
// releases the consumed segments. |
|
instance.setLastProcessedSeqTxn(advanceTo); |
|
instance.setAppliedWatermark(advanceTo); |
|
try { |
The seal call sits behind appendedRows > 0 at both drain sites:
|
if (lvConsumedPersisted && appendedRows > 0) { |
|
if (lvConsumedPersisted && appendedRows > 0) { |
The duration trigger is evaluated only inside that call, so it never runs for a view that emits nothing, and no timer drives such a view:
|
if (!(force || firstCp || restoredHeadFirstFlush || rowTrigger || durationTrigger)) { |
WalPurgeJob floors the purge at the minimum of the consumed watermark, the head checkpoint's base seqTxn and the timeline floor, so the consumed watermark alone releases nothing:
|
final long headBaseSeqTxn = instance.getHeadCheckpointBaseSeqTxn(); |
|
if (headBaseSeqTxn > -1) { |
|
safeToPurgeTxn = Math.min(safeToPurgeTxn, headBaseSeqTxn); |
|
} |
|
// The versioned timeline keeps both A/B generations recoverable. |
|
// Its floor is therefore the minimum normalized base coordinate of |
|
// both durable slots, published to the instance only after the |
|
// superblock commit point. Recovery/repair owners may lower the same |
|
// floor while pinned. Kept as a separate arm from the head above: the |
|
// head follows the newest boundary this process sealed, while the |
|
// timeline floor follows what either durable slot still needs. |
|
final long timelineFloor = instance.getCheckpointTimelineWalPurgeFloor(); |
|
if (timelineFloor > -1) { |
|
safeToPurgeTxn = Math.min(safeToPurgeTxn, timelineFloor); |
|
} |
server.conf describes cairo.live.view.checkpoint.max.duration.micros as a "Wall-clock ceiling between consecutive head-checkpoint writes" that "Caps the worst-case O3 and restart replay window for low-rate views".
Proposed fix
Evaluate the duration trigger on a cycle that consumed commits and emitted nothing, and seal a head checkpoint at the consumed seqTxn. The filter rejected those rows before they reached the window, so the sealed state is the previous root's at a later base coordinate.
Version
master at 6c63556431 (10.0.2-SNAPSHOT). The files involved are unchanged at 77c6cf2d6b.
Found while reviewing #6939. That PR does not change this behaviour for in-order ingest.
Summary
A live view writes a head checkpoint only on a refresh cycle that emitted rows. A view whose filter rejects every row of a stretch of base ingest keeps consuming the commits, but its head checkpoint stays where the last emitted row left it, and
WalPurgeJobholds the base table's WAL at that checkpoint.The base table's WAL segments and sequencer parts then accumulate for as long as no matching row arrives.
cairo.live.view.checkpoint.max.duration.microsdoes not bound it, andlive_views()shows nothing unusual: the view isactivewith zero lag.To reproduce
x = 1.5), in timestamp order.x <= 0), in timestamp order.wal*/<segment>directories ofbase, and compare with a table that has an unfiltered view or no view.These settings make it visible within seconds. They only change the measurement granularity; the default-settings run below shows the same retention.
Observed
On a full
ServerMainat6c63556431, on the wall clock, with the three settings above: 6 accepted commits, then rejected commits every 250 ms for 15 s (15 duration cadences), then 4 s without commits.baselvctlplainlive_views()forlv:view_status = active,lag_seqtxn = 0,lv_consumed_seqtxn = 62,checkpoint_timeline_normalized_base_seqtxn = 1.basestill keeps 61 segments.basekeeps 62 segments. The floor is the minimum over both checkpoint timeline slots, so the WAL is released only after a second seal.DROP LIVE VIEWreleases everything, and so does an invalidation of the view.With the default segment rollover (200,000 rows) and 300 commits of 5,000 rejected rows each,
basekeeps 8 segments (42.0 MB) where the control table keeps 2 (8.4 MB), measured after a restart. That is all base ingest since the pinned commit.The duration setting was also tried at 1 s with
cairo.live.view.checkpoint.rows=1on a hand-driven job: the clock advanced 10 s per commit and then 3 idle hours, and the head checkpoint did not move.Cause
A cycle that emits nothing advances the consumed watermark and returns without sealing. Its comment states an intent that the missing seal defeats: "Advance the watermarks anyway so base WAL retention releases the consumed segments."
questdb/core/src/main/java/io/questdb/cairo/lv/LiveViewRefreshJob.java
Lines 2868 to 2874 in 6c63556
The seal call sits behind
appendedRows > 0at both drain sites:questdb/core/src/main/java/io/questdb/cairo/lv/LiveViewRefreshJob.java
Line 1810 in 6c63556
questdb/core/src/main/java/io/questdb/cairo/lv/LiveViewRefreshJob.java
Line 2177 in 6c63556
The duration trigger is evaluated only inside that call, so it never runs for a view that emits nothing, and no timer drives such a view:
questdb/core/src/main/java/io/questdb/cairo/lv/LiveViewRefreshJob.java
Line 6175 in 6c63556
WalPurgeJobfloors the purge at the minimum of the consumed watermark, the head checkpoint's base seqTxn and the timeline floor, so the consumed watermark alone releases nothing:questdb/core/src/main/java/io/questdb/cairo/wal/WalPurgeJob.java
Lines 630 to 644 in 6c63556
server.confdescribescairo.live.view.checkpoint.max.duration.microsas a "Wall-clock ceiling between consecutive head-checkpoint writes" that "Caps the worst-case O3 and restart replay window for low-rate views".Proposed fix
Evaluate the duration trigger on a cycle that consumed commits and emitted nothing, and seal a head checkpoint at the consumed seqTxn. The filter rejected those rows before they reached the window, so the sealed state is the previous root's at a later base coordinate.
Version
masterat6c63556431(10.0.2-SNAPSHOT). The files involved are unchanged at77c6cf2d6b.Found while reviewing #6939. That PR does not change this behaviour for in-order ingest.