Skip to content

[fix](ivm) Keep the offsets of idle IVM stream partitions alive - #69831

Open
morningman wants to merge 7 commits into
apache:masterfrom
morningman:changelog-monitor-t9
Open

morningman wants to merge 7 commits into
apache:masterfrom
morningman:changelog-monitor-t9

Conversation

@morningman

@morningman morningman commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

What problem does this PR solve?

Issue Number: #69907

Related PR: #69772, #69785

Problem Summary:

In short: an IVM materialized view rebuilds itself completely whenever one of its base table partitions is written after being idle for longer than changelog.ttl_seconds (one day by default), although nothing the refresh needs has expired. This PR has the Master record, for such idle partitions, a keep-alive TSO up to which the partition is known to hold no change, and the refresh checks the TTL against it. Both shared-nothing and cloud mode are covered; MetaService is not changed.

Background

An IVM MV reads each base table through an internal table stream, which keeps a consumption offset (a TSO) per partition. A refresh reads the partition's changelog after the offset and moves the offset to what it read. A table's changelog is kept for changelog.ttl_seconds; a read whose offset is older than now - TTL may have lost changes, so an IVM read refuses it (CHANGELOG_OFFSET_EXPIRED), and since #69772 an AUTO refresh answers that as CHANGELOG_BROKEN and falls back to COMPLETE (a strict INCREMENTAL refresh fails). Since #69785 the same holds for an IVM MV over a duplicate-key table, whose expired offset used to be clipped silently.

The problem, and what it cost

  • The offset of a partition moves only when a refresh consumes the partition, and a refresh only consumes partitions with new data. A partition nobody writes keeps the offset of its last change.
  • The TTL check only compares that offset with the cutoff; it does not know whether anything was written in between. So once a partition has been idle for longer than the TTL, the first write to it makes the next refresh refuse the offset, although the only changes after it are the ones just written, all retained.
  • The cost is a COMPLETE refresh of the whole MV (or a failed strict refresh) for a dimension table updated weekly, a backfill into an older partition, or a quiet weekend.

How this PR fixes it

  • Keep-alive TSO K. For a partition at its end, the Master records K, meaning the partition holds no change in (offset, K]. The read of a refresh moves its start to after K before the TTL check (NormalizeOlapTableStreamScan.applyChangelogTtl). The rows read are the same, and only retained changelog is read. The offset itself does not move, so the invariants built on "offset == partition TSO means caught up" stay as they are.
  • When. IvmStreamKeepAliveDaemon, a Master daemon, runs a round every ivm_stream_keepalive_interval_second (600) over the IVM streams, found from the MV side. A round waits until the TSO service is initialized: a new Master calibrates it in the TSO service's own first cycle, and a round started before that would be lost, with the next one an interval later. A partition qualifies when the base table applies a TTL, the stream has a real offset for it past the initial snapshot, nothing is committed to it after the offset, and the newer of the offset and K has less than half the TTL left. A keep-alive TSO thus gets as old as half the TTL plus one interval before it is renewed, and one interval older for each round that cannot renew it (MetaService unreachable, a lock busy, the TSO service failing), which records nothing and leaves the partition to the next round. A partition is no longer renewed once it is written, so the refresh that reads the write is incremental if it starts within half the TTL minus the interval after the write, one interval less for each such round; with the defaults (a TTL of a day, 600 seconds) that is 11 hours 50 minutes, less ten minutes a round. A refresh that starts later may fall back to COMPLETE, as before this PR. At an interval of half the TTL no time is left at all, so the interval should be well under half the shortest changelog.ttl_seconds of the IVM base tables. The config's description says so.
  • Shared-nothing mode. K is a TSO taken while the base table read lock is held; a commit takes its TSO inside the base table write lock, so every commit the partition has not had yet gets a larger one. A partition with a committed transaction that is not visible yet does not qualify.
  • Cloud mode. The offsets live in MetaService. K is the TSO service's committed watermark. Before the round reads the offsets through get_table_stream_offset (which waits for the partitions' pending transactions), it advances MetaService's TSO fence to K (advance_tso_fence, the fence [fix](binlog) Track committed TSO and fence uncertain commits for bounded incremental reads #67820 added for commits whose outcome the FE does not know). Every commit at or below the watermark is unregistered from the TSO service by then, but one the FE gave up on after a response it may outlive, such as a MetaService transaction that hit the FDB transaction timeout after its commit was sent, can still land. A commit reads the fence in its own FDB transaction and is rejected at or below it, so after the fence every commit at or below K has either landed, and the read shows it, or can no longer land; a commit that lands later gets a larger TSO. A partition whose offset is at its end in the read thus holds no change up to the watermark, for good. A round whose fence fails records nothing. The fence is one MetaService write a round; the commits being applied at that moment conflict on it once and are retried. The read goes in batches of about 10000 partitions; a stream with more partitions is read in one request.
  • K is bound to its offset. K is recorded in FE metadata with the offset it was taken against, and a read uses it only when it starts from that offset (in cloud mode, the offset the read state carries). An offset that moved, or one MetaService reports differently, for example after a restore, leaves K out.
  • Persistence. What a stream holds for a partition -- its offset, the time of its last consumption, the TSO of the initial rows still to be read, and now K -- is one PartitionStreamState in one concurrent map of OlapTableStream (GSON ps), in place of the maps po, pct and hpt and a fourth one for K. A state is replaced as a whole under the stream write lock, so the delta scan of an IVM refresh, which reads the stream without that lock, sees all of one state. Table Stream is not released, so the old fields are not read: a stream from an image written by an earlier build reads back with no partition state and has to be dropped and recreated. The K entries are logged in a new optional list of OP_TABLE_STREAM_CLEANUP and replayed the same way; K of a dropped partition is pruned with its offsets.

Results

The new regression suite mtmv_p0/ivm/test_ivm_stream_keepalive (shared-nothing, run locally): an aggregate MV over a merge-on-write table and an append-only detail MV over a duplicate-key table, both base tables with changelog.ttl_seconds = 60 and a five-second check. Each row is what tasks() reports for the refresh (Status, RefreshMode, IvmFallbackReason), for both MVs, and every MV matches its query over the base table.

Scenario Keep-alive off (the behavior before this PR) Keep-alive on
Idle for 70 seconds, longer than the TTL, then written; REFRESH ... AUTO SUCCESS COMPLETE CHANGELOG_BROKEN: the whole MV is rebuilt SUCCESS, incremental, no fallback
A write lands while a keep-alive round is held (debug point) between what it has read and what it would record -- nothing is recorded for the partition; the refresh after it is incremental and reads the write

The FE log of that run shows the keep-alive TSO renewed every thirty seconds, half the TTL, while the partitions were idle, and nothing recorded for the held stream once released.

Classes, and how they call each other

  • IvmStreamKeepAliveDaemon (new): the Master daemon. It checks every second whether the TSO service is initialized and ivm_stream_keepalive_interval_second has passed since its last round, so that a change of the interval applies at once and the first round after a restart is not lost, and finds the IVM streams through IvmUtil.getIvmStreams (new, which mv_infos() now uses too).
  • TableStreamManager.keepAliveIdlePartitions / keepAliveIdleCloudPartitions (new): one round in each mode; the shared-nothing one takes the locks in the order cleanupStalePartitionOffsets takes them.
  • OlapTableStream: partitionStates (GSON ps), one concurrent map of PartitionStreamState(offset, consumptionTime, historicalTso, keepAlive) in place of the four maps, its keepAlive a PartitionKeepAlive(offset, keepAliveTso); unprotectedKeepAlive, effectiveOffset. A state is replaced as a whole under the stream write lock, but the delta scan of an IVM refresh is created after the statement has locked its tables and reads it without the stream lock. OlapTableStreamUpdate.checkPartitionOffset reads the states.
  • TableStreamCleanupInfo: the new optional keepAliveEntries of OP_TABLE_STREAM_CLEANUP.
  • OlapTableStreamWrapper.getKeepAliveTso and NormalizeOlapTableStreamScan.applyChangelogTtl: the read side.
IvmStreamKeepAliveDaemon (Master, every ivm_stream_keepalive_interval_second, once the TSO service is initialized)
  |-- shared-nothing: TableStreamManager.keepAliveIdlePartitions(stream)
  |       stream read lock -> base table read lock -> TSOService.getTSO() = K -> stream write lock
  |       -> OlapTableStream.unprotectedKeepAlive({partition: offset}, K)
  |       -> EditLog.logTableStreamCleanup(TableStreamCleanupInfo.ofKeepAlive(...))
  `-- cloud: TableStreamManager.keepAliveIdleCloudPartitions(streams)
          TSOService.getStatusSnapshot().getCommittedTso() = K
          -> CloudGlobalTransactionMgr.advanceTsoFence(K)               (MetaService advance_tso_fence)
          -> CloudTableStreamReadStateHelper.getReadStates(batch)       (MetaService get_table_stream_offset)
          -> stream write lock -> unprotectedKeepAlive -> logTableStreamCleanup

replay: OP_TABLE_STREAM_CLEANUP -> TableStreamManager.replayTableStreamCleanup -> replayKeepAlive

read:   IvmDeltaRewriteState / BindRelation -> new OlapTableStreamWrapper(...)   (captures (offset, K))
        [cloud: CloudTableStreamReadStateHook -> installCloudReadStates]          (offset from MetaService)
        NormalizeOlapTableStreamScan.applyChangelogTtl
          -> OlapTableStreamWrapper.getKeepAliveTso(p)   (K only when the read starts from its offset)
          -> start = max(start, K) -> ChangelogUtils.effectiveStartTso(start, cutoff, reject)

Not changed: MetaService, the protos, the offsets themselves, how a user stream is read and consumed (its state moves into the same map, but it gets no K), and the direct incremental query (BindRelation.applyChangelogTtl), which has no offset.

Release note

An IVM materialized view no longer falls back to a COMPLETE refresh when a base table partition is written after being idle for longer than changelog.ttl_seconds. New FE configs enable_ivm_stream_keepalive (default true) and ivm_stream_keepalive_interval_second (default 600), both mutable.

Check List (For Author)

  • Test

    • Regression test

    • Unit Test

    • Manual test (add detailed scripts or steps below)

    • No need to test or manual test. Explain why:

      • This is a refactor/code format and no logic has been changed.
      • Previous test can cover this change.
      • No code files have been changed.
      • Other reason
    • test_ivm_stream_keepalive (new, nonConcurrent): the cases in Results. Run locally in shared-nothing mode, together with the whole mtmv_p0/ivm directory (116 suites, all pass). Not run locally in cloud mode; mtmv_p0 is part of the cloud pipeline, where the same suite takes the cloud branch. Rerun after the TSO readiness fix and the map merge, with the whole mtmv_p0/ivm directory (116 suites), table_stream_p0 (16 suites) and changelog_p0/test_changelog_publish_conflict: all pass; the commit after them, which corrects the description of ivm_stream_keepalive_interval_second, changes no code. The TSO fence only runs in cloud mode and is covered by the unit tests below.

    • TableStreamManagerKeepAliveTest (new, 19 cases): the conditions of both modes (a change after the offset, a committed transaction that is not visible, the initial snapshot, the margin, no TTL or one that never expires an offset; in cloud mode a state that is not at its end or has no real offset, a watermark not past the offset, no calibrated TSO, a failed read, reads in batches, the fence advanced to the watermark once a round before any read, and a round that records nothing when the fence fails), K taken against another offset not counting, the wrapper using K only for a read from its offset in both modes, replay, pruning, a cleanup entry written before K, and an offset with its K read back from an image into a concurrent map. The three cases of the review fixes fail on the code before them.

    • IvmStreamKeepAliveDaemonTest (new): no round while the TSO service is not initialized, a round at the first check after it is, and none again before the interval. It fails without the check, and with the check placed after the round timer is set.

    • ExplainTableStreamPlanTest: an IVM read of an expired offset with a fresh K is admitted and starts after K; one whose K expired too, or was taken against another offset, is rejected.

    • The IVM unit tests, every catalog/stream test class and the other classes using the changed code (57 classes, 931 cases) pass; checkstyle passes for fe-core and fe-common.

  • Behavior changed:

    • No.
    • Yes. A refresh reads an idle partition incrementally where it used to fall back to COMPLETE (or fail, if strict). The state a table stream holds for its partitions is persisted in a new form, and a stream created by an earlier build reads back without its offsets (Table Stream is not released).
  • Does this need documentation?

    • No.
    • Yes. The two FE configs; documentation will follow.

Check List (For Reviewer who merge this PR)

  • Confirm the release note
  • Confirm test cases
  • Confirm document
  • Add branch pick label

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69772, apache#69785

Problem Summary:

An IVM stream records an offset for a partition when a refresh consumes
it, and a refresh only consumes the partitions that have new data, so the
offset of a partition nobody writes stays at its last change. The changelog
TTL check (`ChangelogUtils.effectiveStartTso`) only compares that offset
with the cutoff, `now - changelog.ttl_seconds` (one day by default). Once a
partition has been idle for longer than the TTL, the next write to it makes
the refresh after it reject the offset as expired, and since apache#69772 the
refresh falls back to COMPLETE and rebuilds the whole MV, although nothing
it needs is gone: the only changes after the offset are the ones just
written, all inside the retention. A dimension table updated weekly, a
backfill into an older partition or a quiet weekend is enough. With apache#69785
this also holds for an IVM MV over a duplicate-key table, whose expired
offset used to be clipped.

This commit records a keep-alive TSO K for such partitions, meaning the
partition holds no change in (offset, K]:
- `IvmStreamKeepAliveDaemon`, a Master daemon, finds the IVM streams from
  the MV side (`IvmUtil.getIvmStreams`, which `mv_infos()` now uses too)
  and runs a round every `ivm_stream_keepalive_interval_second` (600). It
  checks every second whether the interval has passed, so a change of the
  interval applies at once. `enable_ivm_stream_keepalive` (true) turns it
  off. Both configs are mutable.
- A partition qualifies when the base table applies a TTL, the stream has a
  real offset for it past the initial snapshot, the partition holds no
  change after the offset, and the newer of the offset and K has less than
  half the TTL left.
- Shared-nothing mode (`TableStreamManager.keepAliveIdlePartitions`): K is
  a TSO taken from the TSO service while the base table read lock is held.
  A commit takes its TSO inside the base table write lock, so every commit
  the partition has not had yet gets a larger one. The locks are taken in
  the order the stale-offset cleanup takes them, and a partition with a
  committed transaction that is not visible yet does not qualify.
- Cloud mode (`TableStreamManager.keepAliveIdleCloudPartitions`): the
  offsets live in MetaService. K is the committed TSO watermark of the TSO
  service, taken before the offsets are read, in batches of up to 10000
  partitions, through `get_table_stream_offset`, which waits for pending
  transactions. Every commit at or below the watermark has finished before
  the read, so a partition whose offset is at its end in the read holds no
  change up to the watermark, and a commit that lands later gets a larger
  TSO. MetaService is not changed.
- K is recorded in FE metadata in both modes, with the offset it was taken
  against: a new GSON field of `OlapTableStream`, logged in a new optional
  entry list of `OP_TABLE_STREAM_CLEANUP` and replayed the same way. A
  read uses K only when it starts from that offset
  (`OlapTableStreamWrapper.getKeepAliveTso`), which in cloud mode is the
  offset the installed read state carries. An offset that moved, or one
  MetaService reports differently after a restore, leaves K out. K taken
  against the same offset only grows, and is pruned with the offsets of a
  dropped partition.
- `NormalizeOlapTableStreamScan.applyChangelogTtl` moves the start of a
  read to after K before the expiration check. The read returns the same
  rows, since the partition has no change in between, and reads retained
  changelog only.

### Release note

An IVM materialized view no longer falls back to a COMPLETE refresh when a
base table partition is written after being idle for longer than
`changelog.ttl_seconds`. New FE configs `enable_ivm_stream_keepalive` and
`ivm_stream_keepalive_interval_second`.

### Check List (For Author)

- Test: Regression test / Unit Test
    - `test_ivm_stream_keepalive` (new, nonConcurrent): an aggregate MV
      over a merge-on-write table and an append-only detail MV over a
      duplicate-key table, a 60-second TTL and a 5-second check. Idle for
      70 seconds and then written, the AUTO refresh of both is incremental
      with no fallback; with the keep-alive off it falls back to COMPLETE
      as CHANGELOG_BROKEN; a write landing while a round is held between
      what it read and what it records is read by the refresh after it.
      Run locally in shared-nothing mode; the whole `mtmv_p0/ivm`
      directory (116 suites) passes. Not run in cloud mode locally.
    - `TableStreamManagerKeepAliveTest` (new, 16 cases): the conditions of
      both modes (a change after the offset, a committed transaction that
      is not visible, the initial snapshot, the margin, no TTL or one that
      never expires an offset; in cloud mode a state that is not at its
      end or has no real offset, a watermark not past the offset, no
      calibrated TSO, a failed read, reads in batches), K taken against
      another offset not counting, the wrapper using K only for a read
      from its offset in both modes, replay, pruning, and state written
      before K.
    - `ExplainTableStreamPlanTest`: an IVM read of an expired offset with
      a fresh K is admitted and starts after K; one whose K expired too,
      or was taken against another offset, is rejected.
    - The IVM unit tests, every `catalog/stream` test class and the other
      classes using the changed code (47 classes, 677 cases) pass;
      checkstyle passes for fe-core and fe-common.
- Behavior changed: Yes. A refresh reads an idle partition incrementally
  where it used to fall back to COMPLETE (or fail, if strict).
- Does this need documentation: Yes, the two FE configs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor

Thank you for your contribution to Apache Doris.
Don't know what should be done next? See How to process your PR.

Please clearly describe your PR:

  1. What problem was fixed (it's best to include specific error reporting information). How it was fixed.
  2. Which behaviors were modified. What was the previous behavior, what is it now, why was it modified, and what possible impacts might there be.
  3. What features were added. Why was this function added?
  4. Which code was refactored and why was this part of the code refactored?
  5. Which functions were optimized and what is the difference before and after the optimization?

…second against changelog.ttl_seconds

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69831

Problem Summary:

The keep-alive daemon renews a partition once less than half of its base
table's changelog.ttl_seconds is left, and only runs a round every
ivm_stream_keepalive_interval_second, so a keep-alive TSO gets as old as
half the TTL plus one interval before it is renewed. It stays valid only
while the interval is at most half the TTL. With the defaults (a TTL of a
day, an interval of ten minutes) that holds, but a table whose TTL is
shorter than twice the interval sees its keep-alive TSO expire between two
rounds, and a refresh after an idle period falls back to COMPLETE as it
did before the keep-alive. Nothing in the config said so.

This commit says so in the description of the config: keep it at most half
the shortest changelog.ttl_seconds of the IVM base tables, what happens
otherwise, that a shorter interval costs more checks but not more edit
logs, and that a change applies within a second.

### Release note

None

### Check List (For Author)

- Test: No need to test (a config description only); fe-common compiles
  and its checkstyle passes.
- Behavior changed: No
- Does this need documentation: Yes, the same rule goes into the user
  documentation of the config.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

/review

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed all 14 changed files at head 754db89. Request changes for the cloud timeout path that can certify an interval containing a later committed change. Also found an unsynchronized keep-alive-map read that can cause unnecessary full refreshes. Both findings are inline; no existing comments need duplication or blocking-ID carryover. No additional user focus was supplied.

Applicable code-review checkpoints:

  • Goal and evidence: the normal idle-partition path records (C,K) without changing C and uses next(K) for the changelog lower bound. Added planner, manager and regression cases cover the intended benefit; the cloud timeout counterexample still violates committed-data visibility.
  • Scope and reuse: the change is focused on keep-alive, reuses the cleanup journal operation and TSO arithmetic, and extracts existing stream-discovery logic without changing its metadata output.
  • Concurrency: reviewed daemon, planner, commit/publish and cleanup interactions. Local base-read -> bounded stream-write locking follows cleanup order; cloud RPC and journal awaits occur outside metadata locks. The synthesized reader lacks its stream lock, and cloud timeout abandonment breaks the watermark proof, as detailed inline.
  • Lifecycle: Env starts the daemon with master-only services; immutable keep-alive values are replaced, and dropped-partition cleanup prunes the map. No new static initialization or ownership cycle was found.
  • Configuration: both settings are mutable and master-only; the one-second polling loop observes updates. The interval versus TTL constraint is documented, and disabling renewal intentionally retains existing K values.
  • Compatibility: optional Gson fields default safely for older metadata. The IVM guide explicitly exempts pre-January-2027 view compatibility; no new wire format or function-system compatibility issue was identified.
  • Thrift/protobuf IDs: not applicable; no gensrc change.
  • Parallel paths: checked shared-nothing/cloud, MOW aggregate/DUP detail, incremental/history/reset/pre-snapshot reads, excluded triggers and Explain/dry-run. Offset mismatch correctly suppresses K; the cloud timeout path remains unsafe.
  • Conditional checks: initial/sentinel offsets, pending invisible versions, changed offsets, TTL margin, missing watermark and disabled/stale/dropped streams have concrete purposes and corresponding negative cases or traced guards.
  • Test coverage: inspected the 16 new manager tests, planner cases and regression suite. None exercises an in-flight FDB commit after a timeout response or the map-capture race. The debug-point regression also relies on sleeps rather than acknowledging keeper completion.
  • Test results: the new expected aggregate totals, detail rows and refresh outcomes agree with the test statements. Result-file generation provenance was not independently verified.
  • Observability: renewal/failure logs include stream identity, count and K; existing daemon exception and fatal journal handling remain available. No separate logging or metric defect was found.
  • Persistence and failover: traced Gson/image coverage and OP_TABLE_STREAM_CLEANUP decode, write and replay. Replay uses the same mutation; normal cloud epoch fencing is present, but it does not close the same-epoch timeout gap described inline.
  • Data mutations and crashes: real consumption offsets and user-data write protocols remain unchanged. Persisting a false K makes the cloud finding survive later reads/replay; no additional atomicity or metadata-leak issue was verified.
  • FE/BE propagation: no new transmitted variable or scan-bound coordinate; stored TSOs remain inclusive and scan ranges half-open.
  • Performance: scans/copies are linear in streams and partitions, with TTL-based renewal throttling. Whole-stream cloud batches can exceed the nominal 10000-partition target, but no concrete failure from that alone was established. The reader race can trigger an expensive COMPLETE fallback.
  • Other concerns: no further verified defect from the complete diff and upstream/downstream review. Transient cloud batch failures publish no proof; initial empty partitions have initialized version/TSO metadata.

Validation was static only: no builds, tests, benchmarks, source edits or submodule initialization. Convergence and delivery are tracked separately in this invocation; final convergence is declared by the submission metadata after the verification round.

Review converged after 2 round(s).

LOG.info("skip keeping IVM stream offsets alive because the committed TSO is not available");
return Collections.emptyList();
}
long keepAliveTso = tsoStatus.getCommittedTso();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Fence timed-out commits before using the cloud watermark

getCommittedTso() does not currently guarantee that every earlier commit has stopped being in flight. A configured FDB transaction timeout (1031) becomes TXN_TIMEOUT -> KV_TXN_COMMIT_ERR; CloudGlobalTransactionMgr.commitTxn() removes that transaction with abandonCommitTso() (lines 974-975), without the fence used for RPC exceptions. FoundationDB explicitly allows a timed-out commit to finish later.

For an idle partition at C, commit T>C can time out, be unregistered, and let the durable watermark advance to K>=T. If this round reads C=endTso before that commit finishes, it persists (C,K). Once the delayed commit publishes T, the next refresh still matches C but applyChangelogTtl() starts after K, silently dropping that committed change; an expired C that previously forced COMPLETE now passes the TTL check. Fence indeterminate commit responses before unregistering them, or establish an equivalent MS fence before using K as a permanent no-change proof. Please cover the timeout-response path, not only RPC exceptions.

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.

Confirmed. MetaService sets fdb_txn_timeout_ms on every FDB transaction, a 1031 during the commit comes back as KV_TXN_COMMIT_ERR, and commitTxn() unregisters that attempt with abandonCommitTso(), so the watermark can pass a commit that still lands. A round that reads the offsets in that window records a K that hides the commit, and the refresh after it skips the commit for good.

Fixed in 19b8e52: keepAliveIdleCloudPartitions now advances MetaService's TSO fence to K (advanceTsoFence) before it reads any offset. A commit reads the fence key in its own FDB transaction and is rejected at or below the fence, so once the fence is advanced, every commit at or below K has either landed, and the read that follows shows it, or can no longer land. A round whose fence fails records nothing. The cost is one fence write a round: commits being applied at that moment conflict on the key once and are retried through the existing KV_TXN_CONFLICT loop. TableStreamManagerKeepAliveTest now checks that the fence is advanced to the watermark once a round, before any read, and that a round whose fence fails reads and records nothing; both cases fail on the code before the fix. The javadoc and the PR description no longer claim that every commit at or below the watermark has finished.

The commit path itself still treats that timeout as a definite failure. That also affects the bounded cloud @incr reads, which admit an end at or below the watermark, and it predates this PR (#67820), so it is left to the commit path and will be followed up separately. The keep-alive round no longer depends on it.

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.

Follow-up on the commit path: #69925 makes commitTxn() advance the TSO fence before it gives up the TSO of a commit that MetaService answers with KV_TXN_COMMIT_ERR, as it already does when the RPC fails, so the watermark no longer passes a commit that can still land. The keep-alive round keeps its own fence; it does not rely on that fix.

this.outputUpdateMap = buildOutputUpdateMap(selectedPartitionIds);
this.keepAlives = Maps.newHashMap();
for (Long partitionId : selectedPartitionIds) {
OlapTableStream.PartitionKeepAlive keepAlive = stream.getKeepAlive(partitionId);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Snapshot keep-alives under the stream read lock

The IVM refresh starts with a plan such as:

LogicalOlapTableSink(mv)
  LogicalAggregate(...)
    LogicalOlapScan(base)

CollectRelation/StatementContext.lock() lock the base and target tables, but the stream is looked up later by IvmDeltaRewriteState when it constructs this wrapper. That path never acquires the stream read lock. Meanwhile the keeper writes OlapTableStream.partitionKeepAlive, a plain HashMap, under the stream write lock while holding a compatible base-table read lock.

A first keep-alive for another partition can resize that map during this get, transiently hiding an existing fresh K. The wrapper permanently captures its absence; when C is older than the TTL, applyChangelogTtl() rejects it and AUTO needlessly rebuilds the whole MV (strict INCREMENTAL fails). Capture the offsets and keep-alives under the stream read lock, or publish a concurrency-safe snapshot; explicitly bound SQL streams are locked, but these synthesized IVM readers are not.

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.

Confirmed. IvmDeltaRewriteState looks the IVM stream up and builds the wrapper after CollectRelation has collected the statement's tables, so the stream is never locked there, and the keeper's put can resize the HashMap under that get. The effect is the one you describe: a needless COMPLETE fallback, or a failed strict refresh, when the missed K is what kept an expired offset readable. No row is read wrong.

Fixed in 6cb029c: partitionKeepAlive is now declared as a ConcurrentMap, so a get never misses an existing entry while another one is added, and the planner path takes no new lock. The field is declared as one, not only initialized as one, so GSON also reads it back from an image as a concurrent map, as it does Database.name2Function. testKeepAliveTsoReadsBackFromTheImage checks that, and it fails with the field declared as a plain Map. The race itself has no deterministic test.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 74.19% (230/310) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28885 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 754db89a5bcd7c85006bd08f1745a0bf7b8414fe, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17593	5172	5046	5046
q2	2151	314	252	252
q3	10227	1399	836	836
q4	4674	466	337	337
q5	7465	810	550	550
q6	185	188	161	161
q7	766	807	610	610
q8	9268	1358	1091	1091
q9	5486	4259	4191	4191
q10	6843	1299	1026	1026
q11	434	258	231	231
q12	643	412	302	302
q13	18067	3733	3046	3046
q14	269	276	251	251
q15	q16	743	727	664	664
q17	1242	757	576	576
q18	6975	6181	6129	6129
q19	1101	973	621	621
q20	401	348	227	227
q21	5980	2465	2558	2465
q22	414	338	273	273
Total cold run time: 100927 ms
Total hot run time: 28885 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	6020	5991	6063	5991
q2	714	616	586	586
q3	5677	5698	5402	5402
q4	2045	2177	1619	1619
q5	5776	5881	5566	5566
q6	253	182	148	148
q7	2271	2030	1828	1828
q8	3080	2657	2795	2657
q9	7921	7675	7458	7458
q10	3633	3645	3152	3152
q11	589	414	387	387
q12	691	674	519	519
q13	3370	3663	3070	3070
q14	287	283	264	264
q15	q16	666	688	643	643
q17	7912	7077	7039	7039
q18	13046	12202	12885	12202
q19	943	833	841	833
q20	2156	2151	1901	1901
q21	5698	4588	4857	4588
q22	521	440	425	425
Total cold run time: 73269 ms
Total hot run time: 66278 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 152959 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 754db89a5bcd7c85006bd08f1745a0bf7b8414fe, data reload: false

query5	4315	619	466	466
query6	440	218	195	195
query7	4807	480	244	244
query8	325	174	153	153
query9	8788	3225	3199	3199
query10	474	315	283	283
query11	6099	3974	3665	3665
query12	146	95	90	90
query13	1262	445	330	330
query14	6140	4040	3712	3712
query14_1	3489	3437	3542	3437
query15	210	195	186	186
query16	956	452	386	386
query17	842	633	509	509
query18	2426	433	338	338
query19	193	160	146	146
query20	86	87	84	84
query21	220	136	110	110
query22	13051	13064	12773	12773
query23	13587	13093	12590	12590
query23_1	12606	12483	12628	12483
query24	7340	852	473	473
query24_1	479	475	472	472
query25	518	405	353	353
query26	1265	262	138	138
query27	2778	454	274	274
query28	4613	2041	2051	2041
query29	1559	591	448	448
query30	313	235	191	191
query31	1014	960	840	840
query32	154	98	90	90
query33	507	302	239	239
query34	928	890	511	511
query35	739	750	648	648
query36	832	808	738	738
query37	123	103	84	84
query38	1857	1780	1702	1702
query39	819	807	767	767
query39_1	753	748	752	748
query40	242	135	101	101
query41	84	72	71	71
query42	99	90	85	85
query43	353	377	319	319
query44	1361	727	726	726
query45	189	202	168	168
query46	848	990	585	585
query47	3024	2897	2617	2617
query48	307	324	232	232
query49	590	424	319	319
query50	700	298	219	219
query51	10385	10682	10345	10345
query52	87	93	75	75
query53	196	215	155	155
query54	244	245	190	190
query55	72	70	75	70
query56	285	245	239	239
query57	1677	1645	1608	1608
query58	317	286	276	276
query59	2144	2138	2045	2045
query60	310	271	253	253
query61	178	166	171	166
query62	391	341	292	292
query63	189	164	169	164
query64	2900	1076	777	777
query65	3483	3382	3347	3347
query66	1791	421	297	297
query67	20638	20652	20323	20323
query68	2991	1020	626	626
query69	413	309	252	252
query70	870	800	785	785
query71	294	239	212	212
query72	2783	2678	2305	2305
query73	531	529	309	309
query74	4681	4542	4377	4377
query75	2402	2344	1944	1944
query76	2322	1050	652	652
query77	359	366	276	276
query78	10504	10112	9475	9475
query79	1008	878	545	545
query80	1250	429	357	357
query81	609	340	281	281
query82	825	135	107	107
query83	310	201	192	192
query84	316	121	100	100
query85	874	429	371	371
query86	403	236	229	229
query87	2043	1980	1831	1831
query88	3884	2786	2796	2786
query89	344	287	251	251
query90	1913	176	172	172
query91	180	154	127	127
query92	105	93	93	93
query93	1114	1032	585	585
query94	664	313	245	245
query95	582	399	299	299
query96	684	501	239	239
query97	2459	2435	2340	2340
query98	167	160	154	154
query99	777	755	633	633
Total cold run time: 238552 ms
Total hot run time: 152959 ms

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 70.65% (219/310) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.77 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 754db89a5bcd7c85006bd08f1745a0bf7b8414fe, data reload: false

query1	0.01	0.01	0.00
query2	0.10	0.05	0.04
query3	0.26	0.09	0.08
query4	1.60	0.10	0.10
query5	0.25	0.25	0.25
query6	1.19	0.69	0.68
query7	0.04	0.00	0.00
query8	0.06	0.04	0.03
query9	0.40	0.36	0.36
query10	0.60	0.59	0.61
query11	0.20	0.14	0.14
query12	0.19	0.14	0.15
query13	0.50	0.50	0.49
query14	1.02	1.01	1.01
query15	0.63	0.62	0.64
query16	0.37	0.35	0.36
query17	1.14	1.18	1.18
query18	0.23	0.22	0.22
query19	2.07	2.00	2.02
query20	0.02	0.01	0.02
query21	15.56	0.19	0.13
query22	4.82	0.05	0.05
query23	16.29	0.34	0.12
query24	3.14	0.48	0.45
query25	0.10	0.07	0.05
query26	1.03	0.24	0.16
query27	0.05	0.03	0.04
query28	3.24	0.75	0.38
query29	12.51	4.25	3.39
query30	0.28	0.16	0.11
query31	2.77	0.57	0.34
query32	3.23	0.60	0.51
query33	3.23	3.28	3.25
query34	15.35	4.07	3.40
query35	3.34	3.33	3.38
query36	0.56	0.44	0.42
query37	0.09	0.06	0.07
query38	0.05	0.04	0.04
query39	0.04	0.03	0.03
query40	0.19	0.15	0.14
query41	0.08	0.04	0.03
query42	0.04	0.02	0.03
query43	0.04	0.04	0.04
Total cold run time: 96.91 s
Total hot run time: 24.77 s

morningman and others added 2 commits October 29, 2026 22:19
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69831

Problem Summary:

`OlapTableStream.partitionKeepAlive` was a `HashMap` that the keep-alive daemon writes under the stream write lock. The delta scan of an IVM refresh reads it without the stream lock: `IvmDeltaRewriteState` looks the stream up and creates its `OlapTableStreamWrapper` after the statement has locked its tables, so the stream is never among them, and the base table read lock the daemon also holds in shared-nothing mode does not exclude the refresh, which holds it too. A put that adds a partition and resizes the map can make a concurrent get miss the keep-alive TSO of another partition. The wrapper keeps the miss for the whole refresh, and when that partition's offset is older than the TTL the read is refused: an AUTO refresh falls back to COMPLETE and a strict INCREMENTAL refresh fails, although a fresh keep-alive TSO was recorded. No row is read wrong.

The field is now declared as a `ConcurrentMap`, so a get never misses an entry while another one is added. Declared as such, GSON reads it back from an image as a concurrent map too, as it does `Database.name2Function`.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - `TableStreamManagerKeepAliveTest`: a keep-alive TSO written to an image reads back, into a concurrent map; the cases that set the field directly set a concurrent map. The race itself has no deterministic test.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…ve round reads the offsets

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69831, apache#67820

Problem Summary:

In cloud mode a keep-alive round of the IVM streams takes the committed TSO watermark as the keep-alive TSO K, reads the offsets from MetaService, and records K for every partition whose offset is at the end of the partition, as a lasting proof that the partition holds no change in (offset, K]. The watermark only says that no commit at or below it is still registered with the TSO service, and a commit can be unregistered while it may still land: a MetaService transaction that hits the FDB transaction timeout (1031) after its commit was sent comes back as `KV_TXN_COMMIT_ERR`, and `CloudGlobalTransactionMgr.commitTxn` unregisters it without the fence it uses for an RPC failure, though FoundationDB documents that such a commit can be committed later. If a round reads the offsets in that window, it records a K past the commit. Once the commit lands, the next refresh starts after K and skips it, and its offset then moves past it: the MV silently misses that write until a COMPLETE refresh. Before the keep-alive, a refresh whose offset had expired fell back to COMPLETE and read the write.

A round now advances MetaService's TSO fence to K (`advance_tso_fence`) before it reads any offset. A commit reads the fence in its own FDB transaction and is rejected at or below it, so after the fence every commit at or below K has either landed, and the read shows it, or can no longer land. A round whose fence fails records nothing. The fence is one MetaService write a round; the commits being applied at that moment conflict on it once and are retried, as on any commit conflict. That the commit path unregisters such a commit without a fence also affects the bounded cloud `@incr` reads, which admit an end at or below the watermark; that is left to the commit path.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - `TableStreamManagerKeepAliveTest`: the fence is advanced to the watermark once a round, before any read; a round whose fence fails reads and records nothing; the other cloud cases run with the fence taken.
- Behavior changed: Yes. In cloud mode a keep-alive round advances MetaService's TSO fence to the watermark before it reads the offsets, and records nothing when the fence fails.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28299 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 19b8e52192321fc929bbd3b0a5a17babbe69717c, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17588	5212	5087	5087
q2	2122	307	265	265
q3	10248	1360	837	837
q4	4678	461	333	333
q5	7424	784	521	521
q6	183	186	153	153
q7	728	796	596	596
q8	9247	1243	1023	1023
q9	5529	4186	4167	4167
q10	6811	1276	1033	1033
q11	456	243	223	223
q12	642	395	277	277
q13	18117	3564	3034	3034
q14	275	270	253	253
q15	q16	722	707	649	649
q17	1211	761	576	576
q18	6923	6153	6070	6070
q19	1120	949	609	609
q20	388	337	233	233
q21	4565	2110	2267	2110
q22	342	297	250	250
Total cold run time: 99319 ms
Total hot run time: 28299 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5598	5590	5597	5590
q2	655	585	519	519
q3	5146	5393	4882	4882
q4	1997	2158	1547	1547
q5	5207	5114	5395	5114
q6	282	207	164	164
q7	2449	2074	1885	1885
q8	3079	2738	2679	2679
q9	7957	7880	7933	7880
q10	3731	3831	3236	3236
q11	606	438	413	413
q12	683	688	537	537
q13	3550	3874	3333	3333
q14	316	309	290	290
q15	q16	697	712	652	652
q17	8248	7026	6875	6875
q18	12997	12155	12987	12155
q19	916	804	819	804
q20	2126	2140	1913	1913
q21	5713	4620	4674	4620
q22	480	443	392	392
Total cold run time: 72433 ms
Total hot run time: 65480 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 150759 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 19b8e52192321fc929bbd3b0a5a17babbe69717c, data reload: false

query5	4308	583	428	428
query6	423	199	195	195
query7	4811	437	227	227
query8	318	163	149	149
query9	8755	3221	3215	3215
query10	486	309	255	255
query11	6004	3853	3578	3578
query12	149	95	88	88
query13	1256	452	323	323
query14	6148	3899	3649	3649
query14_1	3417	3451	3388	3388
query15	209	207	183	183
query16	932	439	411	411
query17	859	655	540	540
query18	2421	424	305	305
query19	206	174	140	140
query20	86	83	83	83
query21	213	126	116	116
query22	13011	12969	12781	12781
query23	13377	12738	12433	12433
query23_1	12516	12529	12525	12525
query24	7390	829	499	499
query24_1	488	485	497	485
query25	540	430	333	333
query26	1275	252	130	130
query27	2777	438	267	267
query28	4653	2007	2013	2007
query29	1563	585	429	429
query30	298	229	180	180
query31	999	927	816	816
query32	145	88	85	85
query33	522	296	228	228
query34	889	844	508	508
query35	729	755	640	640
query36	807	802	716	716
query37	121	94	79	79
query38	1754	1706	1663	1663
query39	788	794	737	737
query39_1	733	720	730	720
query40	221	110	97	97
query41	68	66	63	63
query42	86	85	86	85
query43	353	368	318	318
query44	1344	706	707	706
query45	181	178	162	162
query46	832	955	561	561
query47	2812	2806	2780	2780
query48	296	305	221	221
query49	566	390	288	288
query50	680	271	209	209
query51	10486	10464	10230	10230
query52	77	80	68	68
query53	192	211	155	155
query54	246	201	176	176
query55	76	65	65	65
query56	244	215	205	205
query57	1648	1609	1590	1590
query58	276	266	257	257
query59	2150	2141	2045	2045
query60	274	244	224	224
query61	147	151	143	143
query62	385	331	281	281
query63	202	155	152	152
query64	2715	948	781	781
query65	3327	3269	3287	3269
query66	1777	422	302	302
query67	20027	19939	19948	19939
query68	3002	1024	628	628
query69	377	286	243	243
query70	850	793	784	784
query71	288	235	216	216
query72	2998	2637	2314	2314
query73	506	525	294	294
query74	4613	4496	4337	4337
query75	2290	2306	1915	1915
query76	2315	1023	602	602
query77	350	356	274	274
query78	9547	9696	9110	9110
query79	920	872	527	527
query80	482	417	337	337
query81	519	318	273	273
query82	206	142	101	101
query83	275	191	173	173
query84	294	120	103	103
query85	775	420	370	370
query86	261	233	231	231
query87	1962	1991	1809	1809
query88	3712	2767	2738	2738
query89	317	282	242	242
query90	2122	200	164	164
query91	163	149	132	132
query92	98	88	96	88
query93	993	972	569	569
query94	459	316	263	263
query95	574	387	304	304
query96	692	494	241	241
query97	2439	2416	2323	2323
query98	161	152	149	149
query99	757	765	651	651
Total cold run time: 233213 ms
Total hot run time: 150759 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.23 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 19b8e52192321fc929bbd3b0a5a17babbe69717c, data reload: false

query1	0.01	0.01	0.00
query2	0.09	0.04	0.05
query3	0.26	0.09	0.08
query4	1.61	0.09	0.11
query5	0.24	0.25	0.24
query6	1.17	0.69	0.67
query7	0.04	0.01	0.01
query8	0.06	0.04	0.04
query9	0.40	0.34	0.36
query10	0.56	0.62	0.62
query11	0.20	0.15	0.14
query12	0.18	0.15	0.14
query13	0.50	0.50	0.49
query14	1.03	1.01	1.03
query15	0.62	0.61	0.61
query16	0.36	0.36	0.35
query17	1.13	1.10	1.12
query18	0.20	0.20	0.20
query19	2.04	1.95	2.03
query20	0.01	0.01	0.01
query21	15.50	0.20	0.14
query22	4.84	0.06	0.05
query23	16.27	0.33	0.12
query24	2.86	0.59	0.28
query25	0.10	0.07	0.05
query26	1.09	0.21	0.15
query27	0.05	0.03	0.05
query28	3.26	0.80	0.35
query29	12.50	4.20	3.37
query30	0.28	0.13	0.09
query31	2.77	0.56	0.33
query32	3.22	0.60	0.52
query33	3.24	3.13	3.26
query34	15.53	4.03	3.38
query35	3.36	3.34	3.33
query36	0.56	0.44	0.45
query37	0.09	0.07	0.06
query38	0.05	0.04	0.03
query39	0.04	0.04	0.03
query40	0.17	0.15	0.14
query41	0.09	0.03	0.03
query42	0.04	0.02	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.66 s
Total hot run time: 24.23 s

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 74.45% (236/317) 🎉
Increment coverage report
Complete coverage report

@hello-stephen

Copy link
Copy Markdown
Contributor

FE Regression Coverage Report

Increment line coverage 69.72% (221/317) 🎉
Increment coverage report
Complete coverage report

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review completed at PR head 19b8e52192321fc929bbd3b0a5a17babbe69717c; convergence was reached after 2 bounded review rounds.

The change achieves its main goal for normal operation: idle IVM stream partitions can retain a no-change interval through shared-nothing locks or the cloud TSO fence, and the persisted keep-alive state is replay-compatible. The shared-nothing lock ordering, cloud fence/read-state ordering, offset matching, TTL successor arithmetic, duplicate-key and unique-key paths, stream enumeration, and partition pruning were all traced. No data-loss, stale-proof, deadlock, persistence-format, or non-IVM leakage issue was substantiated.

One new lifecycle issue is reported inline: the IVM daemon starts before the TSO daemon and both pass the readiness gate concurrently. A first cycle can run before TSO calibration, set lastRoundMs, and then suppress retry for the configured 600-second interval. This can leave an offset near its TTL unrenewed after restart, causing strict incremental refresh to fail or AUTO refresh to fall back to COMPLETE. The fix should gate on TSO readiness or advance the timestamp only after a usable round.

The earlier cloud timeout-fence P1 (4410495902) and keep-alive HashMap race P2 (4410495912) were independently revalidated as fixed at this head, so neither is reposted. The user supplied no additional focus beyond the complete review. Persistence compatibility is additive, dynamic configuration is polled, and the added unit/regression tests were inspected but not executed because this invocation is static-only.

Review converged after 2 round(s).

if (now - lastRoundMs < Config.ivm_stream_keepalive_interval_second * 1000L) {
return;
}
lastRoundMs = now;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retry keep-alive after TSO startup readiness

startMasterOnlyDaemonThreads() starts this daemon before tsoService, and both threads only leave MasterDaemon's readiness wait after Env.isReady is set. The first keep-alive cycle can therefore run while TSOService.isInitialized() is still false: local getTSO() throws (cloud returns an uninitialized status). Because lastRoundMs = now is assigned before keepAliveIdlePartitions(), the one-second daemon cycle then suppresses retries until ivm_stream_keepalive_interval_second (600 seconds by default). An idle offset close to its TTL can miss renewal after an FE restart and make the next strict incremental refresh fail or AUTO refresh fall back to COMPLETE. Gate the round on TSO readiness or only advance lastRoundMs after a successful round so calibration failures retry promptly.

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.

Confirmed. startMasterOnlyDaemonThreads() starts this daemon before tsoService, both leave the readiness wait together, and the TSO service calibrates itself in its own first cycle: it logs the next allocation window and, in cloud mode, advances MetaService's TSO fence. A first check that came before that set lastRoundMs and then lost the round (in shared-nothing mode getTSO() threw at the first stream with a partition due for renewal, in cloud mode the uninitialized status skipped it), so the next round came a whole ivm_stream_keepalive_interval_second later. With the default TTL of a day that delay is harmless, but with a changelog.ttl_seconds close to twice the interval, which the config description allows, an idle offset could expire in between: an AUTO refresh falls back to COMPLETE and a strict INCREMENTAL refresh fails. No row is read wrong.

Fixed in cf84aba: runAfterCatalogReady now returns before it sets lastRoundMs while TSOService.getStatusSnapshot().isInitialized() is false, so the first one-second check after calibration starts the round. IvmStreamKeepAliveDaemonTest covers it: no round while the TSO service is not initialized, a round at the first check after it is, and none again before the interval; it fails without the check.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Confirmed. The ordering makes this real rather than theoretical: Env.startMasterOnlyDaemonThreads() starts this daemon before tsoService, and MasterDaemon.runOneCycle() only waits for Env.isReady(), which does not include TSO calibration. The first cycle after an FE restart or a leadership change can therefore run while the service is still uncalibrated: local getTSO() throws ("TSO timestamp is not calibrated"), and in cloud getStatusSnapshot().isInitialized() is false. runAfterCatalogReady() assigns lastRoundMs = now before keepAliveIdlePartitions(), so that skipped cycle suppresses the next attempt for the whole ivm_stream_keepalive_interval_second, and Daemon.run() swallows the throwable and keeps the thread alive, which is what makes it silent.

The impact is bounded but not zero: the lost round only matters when an offset is already inside its renewal margin and the remaining TTL is shorter than the interval. With the default 24h TTL that is 12h of margin against 600s, so it is harmless; it bites a table with a short changelog.ttl_seconds, where the first refresh after the restart fails as strict INCREMENTAL or falls back to COMPLETE although the keep-alive would have covered it.

Suggested fix, so that a round the service could not run counts as "not run", and readiness is checked before any lock is taken:

long now = System.currentTimeMillis();
if (now - lastRoundMs < Config.ivm_stream_keepalive_interval_second * 1000L) {
    return;
}
if (!Env.getCurrentEnv().getTSOService().getStatusSnapshot().isInitialized()) {
    return; // do not advance lastRoundMs: retry on the next one-second cycle
}
lastRoundMs = now;
keepAliveIdlePartitions();

Gating up front also avoids two consequences of taking the TSO mid-round: getTSO() retries with sleeps (200ms per attempt, or tso_service_update_interval_ms) while keepAliveIdlePartitions holds the base-table read lock, so a failure there stalls writers on that table for up to tso_max_get_retry_count sleeps; and one stream's throw aborts the round for every stream after it, because the loop in IvmStreamKeepAliveDaemon.keepAliveIdlePartitions() has no per-stream guard.

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.

Thanks. That gate is what cf84aba added, checked before the interval so that the first one-second check after calibration starts the round; IvmStreamKeepAliveDaemonTest covers it. One small correction on "silent": Daemon.run() logs the throwable at ERROR before it carries on, so the lost round did show in fe.log.

On the two other points, at the current head:

  • getTSO() does not sleep while the service is uncalibrated: it throws before its retry loop. Once calibrated, the only sleep a round of this Master daemon can meet is a logical counter overflow within one physical tick, and then every commit runs the same loop inside the table write lock, so the writers already wait on their own TSOs; a round takes one TSO per stream. K cannot move out of the base table read lock either: a commit takes its TSO inside the write lock, and that is what makes every commit the partition has not had yet get a larger TSO than K.
  • With the gate, the only call on the per-stream path that throws is getTSO(), and its failures (disabled, uncalibrated, retries exhausted) are failures of the whole TSO service, so every stream after it would fail the same way; a per-stream catch would turn one ERROR into one per stream. Anything else that throws there is a bug we want to see rather than step over. A lost round is left to the next one, and the config description now counts that against the margin (00befb0).

morningman and others added 2 commits October 30, 2026 15:28
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69831

Problem Summary:

`startMasterOnlyDaemonThreads()` starts `IvmStreamKeepAliveDaemon` before the TSO service, and both wait for `Env.isReady` before their first cycle. The TSO service calibrates itself in that first cycle, which writes the next allocation window to the edit log and, in cloud mode, advances MetaService's TSO fence, so the first keep-alive check after an FE restart or a master change can come before it. The daemon set `lastRoundMs` before running the round, so such a round was spent for nothing: in shared-nothing mode `getTSO()` threw "TSO timestamp is not calibrated" at the first stream with a partition due for renewal, which ended the round, and in cloud mode the round was skipped because the TSO status was not initialized. The next round then came `ivm_stream_keepalive_interval_second` (600 seconds by default) later. With a `changelog.ttl_seconds` close to twice the interval, which the config description allows, an idle offset could expire before that round: an AUTO refresh fell back to COMPLETE and a strict INCREMENTAL refresh failed. No row was read wrong.

A round now waits until the TSO service is initialized, without setting `lastRoundMs`, so the first check after the TSO service is ready starts it.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - `IvmStreamKeepAliveDaemonTest`: no round starts while the TSO service is not initialized, the first check after it is starts one, and the next round waits for the interval.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
### What problem does this PR solve?

Issue Number: None

Related PR: apache#69831

Problem Summary:

`OlapTableStream` kept what it holds for a partition in four maps keyed by partition id: the offset (`po`), the time of the last consumption (`pct`), the TSO of the initial rows still to be read (`hpt`), and, since this PR, the keep-alive TSO (`pka`). Each place that adds, prunes or collects the partitions of a stream had to walk all four, and a read made without the stream lock, as the delta scan of an IVM refresh does, could see one map updated and another not.

They are now one `ConcurrentMap<Long, PartitionStreamState>` (GSON `ps`). A `PartitionStreamState` holds the offset, the consumption time, the historical TSO and the keep-alive TSO of one partition, and is replaced as a whole under the stream write lock, never changed in place, so a read without the lock sees all of one state. A keep-alive TSO still records the offset it was taken against: in cloud mode the offsets live in MetaService, and the state of a partition holds only its keep-alive TSO.

Table Stream is not released, so the old fields are not read: a stream from an image written by an earlier build reads back with no partition state, as if it had consumed nothing, and has to be dropped and recreated.

### Release note

None

### Check List (For Author)

- Test: Unit Test
    - The table stream and IVM unit tests, and the other classes using the changed code (57 classes, 931 cases), pass; the tests that set the maps directly set the partition states. `TableStreamManagerKeepAliveTest` reads an offset and its keep-alive TSO back from an image, into a concurrent map; the case for an image without the keep-alive TSO is gone, and the one for a cleanup entry written before it stays.
- Behavior changed: Yes. The state of the partitions of a table stream is persisted in a new form, and a stream created by an earlier build loses its offsets.
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@morningman

Copy link
Copy Markdown
Contributor Author

/review

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review completed for PR #69831 at exact head bcbfdd134b65179aadcfcb7766a8c10454d2c695, with convergence after 2 bounded rounds.

Verified findings:

  • The OlapTableStream refactor replaces the parent image fields po, pct, and hpt with ps without Gson alternates or migration. Existing user and IVM streams therefore lose offsets, historical TSOs, and consumption timestamps after an FE image/restart; later refreshes can replay retained data or reread rows into an already populated MV.
  • Initialized cloud keep-alive rounds consume lastRoundMs even when the committed watermark is unavailable, fencing fails, or a read-state RPC fails. A transient failure can therefore defer retry for the full configured interval and let a near-expiry offset expire, causing strict incremental failure or AUTO COMPLETE fallback.

Existing inline findings were revalidated against this head and are fixed, so no blocking comment IDs are carried: cloud watermark fencing (4410495902), the wrapper keep-alive map race (4410495912), and TSO startup readiness (4419362329). No additional user focus was supplied. The Nereids TTL/exclusive-bound, historical/empty partition, local locking, cloud response validation/fencing, replay, stream discovery, metadata, and test changes were inspected with no further verified defects. Validation was static only; builds and tests were not executed.

Review converged after 2 round(s).

// partition id -> what the stream holds for the partition. A state is replaced as a whole under the stream write
// lock, never changed in place, and it is also read without the lock: an IVM refresh reads the stream through a
// scan it creates after the statement has locked its tables (OlapTableStreamWrapper). So it is a concurrent map,
// and such a read sees one whole state of a partition.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Preserve table-stream partition state across image upgrades

OlapTableStream used to persist consumed offsets, historical TSOs, and consumption times in po, pct, and hpt. This head reads only the new ps field and has no alternate annotations or migration, so an image/checkpoint written by the parent commit deserializes every existing stream with an empty partitionStates map. After an FE restart or rolling upgrade, a user stream can replay retained changes and an IVM stream is treated as unconsumed; the next refresh can reread current rows into an already populated MV, while expired history is no longer represented. Replay only fills partitions that consume after the restart. Preserve the old fields through Gson migration before exposing the stream state.

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 format change is intended. The PR description says so under Persistence: "Table Stream is not released, so the old fields are not read: a stream from an image written by an earlier build reads back with no partition state and has to be dropped and recreated."

No release carries Table Stream: OlapTableStream is on none of branch-4.0, branch-4.1 and branch-4.2, and enable_table_stream is experimental, off by default and not mutable. So there is no rolling upgrade from a release to cover. A cluster that ran an earlier master build with the switch on drops and recreates its streams and IVM MVs; we do not carry a migration for a format that was never released.

The consequence you describe is right, and it is why such a stream has to be recreated rather than kept. In shared-nothing mode, where the offsets live only in FE metadata, an IVM refresh takes a partition without an offset for one the MV has not read yet: it reads the partition from the base table and appends it, so the MV gets those rows a second time and the refresh still succeeds. A user stream reads its retained changelog again.

// on it once and are retried, as on any commit conflict.
long fenceTso;
try {
fenceTso = Env.getCurrentEnv().getGlobalTransactionMgr().advanceTsoFence(keepAliveTso);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retry failed cloud keep-alive rounds

lastRoundMs is advanced before the cloud keep-alive work, but this path treats several failures as a completed round: an initialized TSO service with committedTso <= 0, a failed advanceTsoFence, and a getReadStates UserException all return without recording a keep-alive. The daemon then waits the full ivm_stream_keepalive_interval_second before trying again. With the documented interval up to half the table TTL, an offset that entered the renewal margin just before a transient MetaService failure can expire before the retry, making strict INCREMENTAL fail or AUTO fall back to COMPLETE. Propagate round success/failure and do not consume the interval on an unsuccessful cloud round (with suitable backoff).

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 mechanism is right: lastRoundMs is set before the round, the three paths you list record nothing (the first two for the whole round, a failed getReadStates for its batch), and the next attempt comes one ivm_stream_keepalive_interval_second later. Shared-nothing mode has the same shape (a getTSO() failure, a busy lock).

Leaving a failed round to the next one is intended. Such a round records nothing (testCloudKeepAliveSkipsRoundWhenTheFenceFails, testCloudKeepAliveSkipsRoundWhenReadStatesFail), and the next round that succeeds renews a partition that is still idle even if its K has expired by then: neither mode's condition asks whether K is within the TTL, and "nothing was committed after the offset" holds however old K is. What a failed round can cost is the refresh that reads a write made after it, which falls back to COMPLETE as before this PR (a strict INCREMENTAL refresh fails); no row is read wrong.

How much that costs is a matter of margin. When an idle partition is written, the newer of its offset and K is at most TTL/2 + one interval old, and one interval older for each round that could not renew it. After the write the partition is not renewed any more, and the refresh that reads the write has to start before that TSO is a TTL old. So that refresh is incremental if it starts within TTL/2 - interval after the write, one interval less per failed round. With the defaults (a TTL of a day, 600 seconds) that is 11 h 50 min, and a refresh right after the write still reads incrementally after 71 failed rounds in a row.

What this comment does show is that the bound in the config description was wrong: "at most half the shortest changelog.ttl_seconds". At half, no time is left even when every round succeeds, and a refresh shortly after the write falls back; retrying failed rounds sooner would not change that. Fixed in 00befb0: the description of ivm_stream_keepalive_interval_second now states the time a refresh has after a write, less one interval for each check that could not renew the partition, and asks for an interval well under half the TTL. The PR description says the same.

A faster retry is not added. A cloud round advances MetaService's TSO fence before it reads, so retrying sooner while MetaService is failing writes to it more often, and the commits being applied at that moment conflict on each fence once. The interval is the retry period; more margin comes from a shorter interval, which costs more checks but no more edit logs.

@hello-stephen

Copy link
Copy Markdown
Contributor

FE UT Coverage Report

Increment line coverage 69.67% (255/366) 🎉
Increment coverage report
Complete coverage report

…ive_interval_second

### What problem does this PR solve?

Issue Number: None

Related PR: apache#69831

Problem Summary:

The description of `ivm_stream_keepalive_interval_second` asked for an interval of at most half the shortest `changelog.ttl_seconds` of the IVM base tables, a bound that leaves a refresh no time. A partition is renewed at the first check after less than half the TTL is left, so when an idle partition is written, the newer of its offset and its keep-alive TSO is up to half the TTL plus one interval old, and one interval older for each check that could not renew it: a round that cannot reach MetaService, finds a lock busy or fails to get a TSO records nothing and leaves the partition to the next one. After the write the partition is not renewed any more, and the refresh that reads the write falls back to COMPLETE (a strict INCREMENTAL refresh fails) once that TSO is older than the TTL. So a refresh is sure to be incremental only if it starts within half the TTL minus the interval after the write, one interval less for each check that could not renew the partition: 11 hours 50 minutes with the defaults (a TTL of a day, 600 seconds), and no time at all at the bound the description gave.

The description now states that time and asks for an interval well under half the TTL. A failed round is still left to the next one: retrying it sooner would not change the bound, and a cloud round first advances MetaService's TSO fence, so a faster retry while MetaService is failing would write to it more often.

### Release note

None

### Check List (For Author)

- Test: No need to test (only the description of a config changes); checkstyle passes for fe-common.
- Behavior changed: No
- Does this need documentation: No

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28893 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit bcbfdd134b65179aadcfcb7766a8c10454d2c695, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17579	5180	5071	5071
q2	2159	306	253	253
q3	10254	1433	840	840
q4	4686	463	340	340
q5	7440	801	530	530
q6	191	188	153	153
q7	756	819	601	601
q8	9247	1309	1119	1119
q9	5430	4209	4187	4187
q10	6828	1281	1048	1048
q11	421	253	228	228
q12	642	404	290	290
q13	18102	3711	3018	3018
q14	274	277	244	244
q15	q16	734	720	666	666
q17	1233	754	578	578
q18	7055	6160	6120	6120
q19	1109	980	620	620
q20	394	347	237	237
q21	5974	2587	2476	2476
q22	372	325	274	274
Total cold run time: 100880 ms
Total hot run time: 28893 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5993	6280	6169	6169
q2	736	577	564	564
q3	5700	5810	5339	5339
q4	2010	2150	1593	1593
q5	5897	5694	5632	5632
q6	260	193	152	152
q7	2265	1986	1799	1799
q8	3127	2657	2662	2657
q9	7723	7495	7435	7435
q10	3631	3564	3191	3191
q11	579	412	388	388
q12	668	694	513	513
q13	3355	3637	3018	3018
q14	296	285	277	277
q15	q16	652	692	614	614
q17	7734	7001	6845	6845
q18	12921	12128	12890	12128
q19	909	838	828	828
q20	2135	2163	1915	1915
q21	5591	4593	4758	4593
q22	502	439	418	418
Total cold run time: 72684 ms
Total hot run time: 66068 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151307 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit bcbfdd134b65179aadcfcb7766a8c10454d2c695, data reload: false

query5	4315	610	442	442
query6	438	229	189	189
query7	4813	450	229	229
query8	312	164	150	150
query9	8783	3188	3192	3188
query10	484	308	268	268
query11	5928	3836	3582	3582
query12	158	92	88	88
query13	1262	467	330	330
query14	6043	3929	3627	3627
query14_1	3449	3440	3453	3440
query15	213	200	181	181
query16	954	494	426	426
query17	895	678	572	572
query18	2435	452	339	339
query19	204	163	138	138
query20	87	86	85	85
query21	211	131	109	109
query22	12958	12895	12734	12734
query23	13577	13060	12411	12411
query23_1	12642	12556	12623	12556
query24	7366	839	476	476
query24_1	484	487	499	487
query25	548	419	373	373
query26	1293	261	140	140
query27	2766	452	278	278
query28	4606	2036	2024	2024
query29	1568	609	483	483
query30	300	225	190	190
query31	1008	936	822	822
query32	157	96	94	94
query33	527	303	252	252
query34	896	866	500	500
query35	759	758	662	662
query36	811	842	727	727
query37	127	103	89	89
query38	1783	1714	1666	1666
query39	796	810	738	738
query39_1	731	727	743	727
query40	217	120	98	98
query41	72	67	72	67
query42	90	87	86	86
query43	345	367	318	318
query44	1316	705	724	705
query45	218	172	168	168
query46	851	988	560	560
query47	2946	2920	2915	2915
query48	294	322	221	221
query49	560	399	291	291
query50	715	280	211	211
query51	10601	10497	10621	10497
query52	78	87	70	70
query53	187	203	148	148
query54	238	186	188	186
query55	73	70	61	61
query56	238	227	206	206
query57	1658	1617	1557	1557
query58	289	270	261	261
query59	2160	2108	1948	1948
query60	292	245	231	231
query61	148	146	146	146
query62	384	340	280	280
query63	187	157	159	157
query64	2775	936	760	760
query65	3351	3258	3281	3258
query66	1755	418	300	300
query67	20005	19985	20067	19985
query68	3040	1035	629	629
query69	393	281	253	253
query70	847	734	771	734
query71	299	232	217	217
query72	2680	2561	2283	2283
query73	517	549	294	294
query74	4654	4549	4349	4349
query75	2345	2298	1903	1903
query76	2289	1016	611	611
query77	350	380	250	250
query78	9838	9739	9144	9144
query79	1021	854	532	532
query80	1364	425	339	339
query81	603	314	274	274
query82	747	137	104	104
query83	305	206	178	178
query84	318	128	102	102
query85	1019	430	365	365
query86	402	234	229	229
query87	2013	1958	1781	1781
query88	3878	2783	2768	2768
query89	330	282	245	245
query90	1832	168	173	168
query91	166	152	132	132
query92	100	88	91	88
query93	1036	1027	569	569
query94	619	305	269	269
query95	577	421	324	324
query96	698	503	241	241
query97	2450	2403	2298	2298
query98	168	155	150	150
query99	768	764	643	643
Total cold run time: 236097 ms
Total hot run time: 151307 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.16 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit bcbfdd134b65179aadcfcb7766a8c10454d2c695, data reload: false

query1	0.00	0.00	0.00
query2	0.09	0.05	0.05
query3	0.27	0.09	0.08
query4	1.61	0.09	0.09
query5	0.23	0.24	0.24
query6	1.17	0.68	0.69
query7	0.05	0.01	0.01
query8	0.06	0.03	0.03
query9	0.39	0.34	0.36
query10	0.57	0.57	0.58
query11	0.20	0.14	0.15
query12	0.17	0.15	0.15
query13	0.49	0.48	0.49
query14	1.02	1.01	1.01
query15	0.62	0.60	0.61
query16	0.35	0.35	0.35
query17	1.13	1.17	1.11
query18	0.20	0.18	0.20
query19	2.05	1.93	2.02
query20	0.02	0.02	0.01
query21	15.47	0.19	0.14
query22	4.85	0.05	0.06
query23	16.27	0.32	0.12
query24	3.27	0.64	0.32
query25	0.10	0.07	0.05
query26	0.98	0.22	0.15
query27	0.04	0.04	0.04
query28	3.04	0.69	0.37
query29	12.48	4.06	3.26
query30	0.27	0.13	0.09
query31	2.77	0.54	0.33
query32	3.23	0.59	0.50
query33	3.24	3.19	3.29
query34	15.34	4.00	3.39
query35	3.31	3.32	3.33
query36	0.54	0.44	0.43
query37	0.09	0.06	0.07
query38	0.05	0.04	0.03
query39	0.04	0.03	0.03
query40	0.17	0.15	0.15
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.05	0.04	0.03
Total cold run time: 96.42 s
Total hot run time: 24.16 s

@morningman

Copy link
Copy Markdown
Contributor Author

run buildall

@morningman

Copy link
Copy Markdown
Contributor Author

/review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Static review completed for PR #69831 at exact head 00befb02e5e9d25a6e42e333baa97365fbf47d4f. No new verified inline findings remain, so this delivery contains no new comments and carries no existing blocking IDs.

The change achieves its stated goal: idle IVM stream partitions can persist a no-change interval and use the exclusive successor of that keep-alive TSO when planning an incremental changelog read. The shared-nothing path snapshots offsets, checks partition visibility under the base-table read lock, captures TSO before releasing it, and rechecks the stream offset before publication. The cloud path fences the committed watermark before complete MetaService read-state batches and records nothing for failed batches. The wrapper applies a keep-alive only when its recorded offset matches the actual read offset.

Critical checkpoint conclusions:

  • Goal and evidence: the changed manager, planner, FE unit, and regression cases cover local/cloud, unique-key aggregate and duplicate-key detail, TTL expiry, replay, and the race boundary. Tests were inspected but not executed because this invocation is static-only.
  • Scope and reuse: the implementation is focused on keep-alive, reuses the existing cleanup journal and TSO utilities, and centralizes IVM stream discovery in IvmUtil.
  • Concurrency: state values are replaced as whole partition snapshots in a concurrent map; local base-table/stream locking and cloud fence/read ordering were traced, with no deadlock or stale-proof defect verified.
  • Lifecycle/configuration: the daemon is constructed and started in the master-only lifecycle, waits for catalog and TSO readiness, and polls both mutable settings on each tick.
  • Persistence/compatibility: keep-alive cleanup entries are optional and replay through the existing journal operation. The OlapTableStream field-format break is intentional for unreleased experimental table streams under the repository IVM compatibility guidance. No thrift/protobuf change is present.
  • Parallel paths and errors: incremental, snapshot, reset, append-only, MOW, cloud, and shared-nothing paths were traced; malformed cloud responses and failed fences produce no keep-alive proof, and non-IVM/user streams are excluded.
  • Observability/performance: renewal and failure paths retain stream identity and TSO logging; the scan/discovery work is bounded by streams and partitions.

Existing review context was revalidated at this head: cloud fence finding 4410495902, wrapper map race 4410495912, and TSO-startup readiness 4419362329 are fixed in the current code. The old-image concern 4422394915 is intentionally unsupported for this pre-release experimental format. The failed-round retry concern 4422394921 remains known context and is not duplicated; it is a P2 margin/behavior discussion rather than a new blocking finding. No additional user focus was supplied.

Validation was static only: no builds, tests, benchmarks, source edits, or submodule initialization. Review coverage converged after 1 bounded round: all assigned subagents completed their full scopes and rechecks, and every candidate and risk focus has an explicit disposition.

Review converged after 1 round(s).

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-H: Total hot run time: 28745 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpch-tools
Tpch sf100 test result on commit 00befb02e5e9d25a6e42e333baa97365fbf47d4f, data reload: false

------ Round 1 ----------------------------------
============================================
q1	17606	5183	5114	5114
q2	2139	325	279	279
q3	10191	1395	832	832
q4	4688	470	336	336
q5	7434	829	548	548
q6	182	186	152	152
q7	743	794	601	601
q8	9268	1355	1060	1060
q9	5592	4237	4212	4212
q10	6833	1296	1048	1048
q11	443	257	235	235
q12	640	408	307	307
q13	18091	3786	3005	3005
q14	278	280	248	248
q15	q16	739	712	675	675
q17	1225	745	568	568
q18	7051	6175	6096	6096
q19	1263	982	630	630
q20	410	361	224	224
q21	5987	2311	2357	2311
q22	355	306	264	264
Total cold run time: 101158 ms
Total hot run time: 28745 ms

----- Round 2, with runtime_filter_mode=off -----
============================================
q1	5639	5616	5682	5616
q2	697	582	539	539
q3	5235	5371	4966	4966
q4	2018	2196	1628	1628
q5	5266	5164	5987	5164
q6	257	194	158	158
q7	2272	2108	1859	1859
q8	3200	2806	2750	2750
q9	8101	8037	7967	7967
q10	3745	3647	3240	3240
q11	630	469	420	420
q12	708	685	560	560
q13	3558	4180	3303	3303
q14	312	314	288	288
q15	q16	700	722	710	710
q17	8264	7639	7287	7287
q18	12976	12184	12798	12184
q19	935	809	802	802
q20	2168	2152	1942	1942
q21	5844	4920	5069	4920
q22	502	455	409	409
Total cold run time: 73027 ms
Total hot run time: 66712 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
TPC-DS: Total hot run time: 151486 ms
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/tpcds-tools
TPC-DS sf100 test result on commit 00befb02e5e9d25a6e42e333baa97365fbf47d4f, data reload: false

query5	4300	582	448	448
query6	428	219	204	204
query7	4800	456	231	231
query8	320	166	154	154
query9	8778	3193	3173	3173
query10	471	316	254	254
query11	5996	3843	3574	3574
query12	151	100	88	88
query13	1271	479	353	353
query14	6175	3936	3639	3639
query14_1	3533	3394	3462	3394
query15	205	206	184	184
query16	942	412	402	402
query17	855	699	544	544
query18	2422	418	314	314
query19	191	165	140	140
query20	87	87	83	83
query21	211	127	111	111
query22	12970	12913	12773	12773
query23	13498	12821	12418	12418
query23_1	12577	12590	12579	12579
query24	7360	825	461	461
query24_1	516	484	473	473
query25	510	396	348	348
query26	1258	258	132	132
query27	2770	443	272	272
query28	4626	2041	2043	2041
query29	1560	587	437	437
query30	288	224	182	182
query31	1013	923	817	817
query32	157	96	90	90
query33	520	290	235	235
query34	891	872	508	508
query35	725	743	641	641
query36	817	842	716	716
query37	119	94	82	82
query38	1789	1734	1668	1668
query39	785	794	736	736
query39_1	726	722	745	722
query40	219	113	106	106
query41	72	63	70	63
query42	85	86	81	81
query43	334	358	314	314
query44	1317	693	697	693
query45	180	178	167	167
query46	824	960	585	585
query47	3011	2710	2852	2710
query48	293	323	217	217
query49	581	401	299	299
query50	683	273	206	206
query51	10686	10619	10612	10612
query52	78	79	69	69
query53	184	212	155	155
query54	253	192	180	180
query55	72	69	66	66
query56	242	214	216	214
query57	1646	1603	1580	1580
query58	281	262	259	259
query59	2125	2124	1992	1992
query60	288	247	210	210
query61	153	145	149	145
query62	397	349	286	286
query63	189	161	160	160
query64	2800	1073	846	846
query65	3324	3271	3267	3267
query66	1762	415	302	302
query67	20155	20067	19962	19962
query68	2897	983	601	601
query69	386	278	252	252
query70	844	802	767	767
query71	294	245	211	211
query72	2766	2612	2311	2311
query73	528	532	299	299
query74	4616	4488	4390	4390
query75	2347	2291	1904	1904
query76	2310	1015	630	630
query77	349	371	278	278
query78	9858	9695	9086	9086
query79	1041	899	536	536
query80	541	435	354	354
query81	549	314	281	281
query82	647	140	109	109
query83	310	193	175	175
query84	313	133	99	99
query85	829	437	376	376
query86	346	240	222	222
query87	1992	1971	1803	1803
query88	3769	2780	2775	2775
query89	332	289	256	256
query90	1909	168	168	168
query91	164	151	127	127
query92	98	89	86	86
query93	1059	989	573	573
query94	446	310	265	265
query95	578	335	357	335
query96	692	483	245	245
query97	2450	2443	2307	2307
query98	166	156	152	152
query99	781	758	634	634
Total cold run time: 234862 ms
Total hot run time: 151486 ms

@hello-stephen

Copy link
Copy Markdown
Contributor
ClickBench: Total hot run time: 24.29 s
machine: 'aliyun_ecs.c7a.8xlarge_32C64G'
scripts: https://github.com/apache/doris/tree/master/tools/clickbench-tools
ClickBench test result on commit 00befb02e5e9d25a6e42e333baa97365fbf47d4f, data reload: false

query1	0.01	0.00	0.00
query2	0.09	0.05	0.05
query3	0.26	0.09	0.08
query4	1.61	0.11	0.10
query5	0.23	0.25	0.24
query6	1.17	0.68	0.68
query7	0.04	0.01	0.00
query8	0.06	0.05	0.04
query9	0.40	0.36	0.36
query10	0.59	0.57	0.61
query11	0.20	0.15	0.14
query12	0.17	0.15	0.15
query13	0.50	0.50	0.51
query14	1.02	1.03	1.00
query15	0.64	0.62	0.60
query16	0.35	0.35	0.36
query17	1.09	1.15	1.16
query18	0.21	0.19	0.19
query19	2.15	1.98	2.03
query20	0.01	0.01	0.02
query21	15.47	0.21	0.14
query22	4.92	0.05	0.06
query23	16.27	0.32	0.12
query24	2.94	0.65	0.36
query25	0.12	0.06	0.05
query26	1.23	0.21	0.16
query27	0.05	0.05	0.04
query28	3.38	0.69	0.36
query29	12.47	4.12	3.23
query30	0.27	0.13	0.09
query31	2.76	0.56	0.33
query32	3.22	0.60	0.50
query33	3.18	3.18	3.18
query34	15.36	4.01	3.40
query35	3.35	3.33	3.35
query36	0.56	0.45	0.42
query37	0.10	0.06	0.06
query38	0.05	0.03	0.04
query39	0.04	0.03	0.02
query40	0.17	0.15	0.14
query41	0.09	0.03	0.03
query42	0.04	0.03	0.03
query43	0.04	0.04	0.03
Total cold run time: 96.88 s
Total hot run time: 24.29 s

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants