Skip to content

[#13893] fix(cache): Recover late change logs and fence cache publication - #13831

Merged
jerryshao merged 12 commits into
apache:mainfrom
yuqi1129:cache-late-commit-replay
Oct 16, 2026
Merged

jerryshao merged 12 commits into
apache:mainfrom
yuqi1129:cache-late-commit-replay

Conversation

@yuqi1129

@yuqi1129 yuqi1129 commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

What changes were proposed in this pull request?

  • Replay recent change-log rows behind the cursor with bounded ID deduplication, diagnostics, and explicit listener opt-in.
  • Start a replay sweep at most once per max(pollIntervalSecs, replayWindowSecs / 4) (15s by default), and expose replay-records-total, replay-failures-total, and dedup-evictions-total metrics.
  • Fence point-read/insert cache publication without requiring reentrant SPI locks, and document that invalidateOnKeyChange may run under the entity's own cache lock.
  • Invalidate local and replayed catalog events while preserving operation leases; update configuration and metrics documentation.

Why are the changes needed?

ID allocation order differs from commit order. Concurrent cache publication and name-based local catalog markers can also leave stale entries.

Fix: #13893

Part of #13828; this PR implements its three P0 fixes and leaves the remaining EPIC work open.

Does this PR introduce any user-facing change?

Adds gravitino.entityChangeLog.replayWindowSecs (default 60; 0 disables replay) and gravitino.entityChangeLog.replayMaxEntries (default 100000), plus three entity-change-log counters: replay-records-total, replay-failures-total, and dedup-evictions-total. Existing third-party listeners remain incremental-only. No schema or cache-TTL change.

Replay adds database reads and possible catalog reloads. Its window uses recorded created_at, including PostgreSQL transaction-start timestamps; long transactions, pruned history, or overload can still lose invalidations. Each sweep rereads the whole window. Throttling sweep starts to a quarter of the window bounds this to about four reads per row per server, instead of window/poll interval (20 by default). The cost: late-commit recovery can take up to one extra sweep interval, and a row is guaranteed to be replayed only if it becomes visible within the window minus one sweep interval (45s by default). A finer gap/low-water-mark scheme was considered and deferred for maintainability. Replay-only full-page cycles still perform incremental, tail, and replay queries. A late row behind an already scanned page waits for the next sweep; the replay window is a history range, not a delivery deadline.

How was this patch tested?

  • Full core suite: 2,761 tests, zero failures/errors, two existing skips. Final focused suite: 39 tests, including the added timing regression, all passed. Previously validated unchanged server-common: 485 tests, all passed.
  • Local H2/MySQL/PostgreSQL late-commit probes and shared-PostgreSQL three-JVM checks for ten cacheable entity types passed.
  • Three P0 regressions fail when old behavior is restored; the non-reentrant SPI regression fails on the initial implementation and passes after the compatibility fix.
  • Default 3s polling: real PostgreSQL/three-JVM probes converged in 2.702s (incremental) and 3.101s (late commit) with small history; the late-commit figure predates sweep throttling, which can add up to one sweep interval. A controlled ordinary-incremental budget test delivers in 5.7s with 0.9s incremental work and 0.9s replay work; moving delivery after replay fails at 6.6s. A separate multi-page late-replay probe takes 7.5s, so these checks do not establish a universal 6s replay deadline.
  • Sweep throttling and metrics: 37 TestEntityChangeLogPoller tests pass, including new throttle, poll-interval floor, and eviction-counter tests; the two throttle tests fail with a zero sweep interval. TestCacheCommitOrdering passes on H2, MySQL, and PostgreSQL.
  • Catalog modules that mock Config (generic lakehouse, fileset, Kafka, model) now stub the replay settings: 29/232/15/37 tests passed; the JMH benchmark compiles.
  • After merging main ([#13776] improvement(core): Report entity-store lock-wait timeouts as conflicts #13792): the new TestRelationalEntityStoreLockWaitTimeout mocks Config and now stubs the replay settings (all 7 cases NPE without them). 14 affected core classes on H2: 204 tests passed.
  • Review round (5a1312f, 6a242d6): the first replay sweep after startup logs at INFO and later sweeps keep the late-commit WARN (pinned in tests; the INFO assertion fails without the change); a failed mid-sweep page resumes from the saved page cursor (fails if the cursor is reset). TestEntityChangeLogPoller (39) plus related store/cache classes: 115 tests passed on H2.
  • Spotless, compilation, and diff checks passed.

Unit-suite command:

LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8 ./gradlew :core:test :server-common:test -PskipITs -PskipDockerTests=true

…ache publication

Signed-off-by: yuqi <yuqi@datastrato.com>
…ility

Signed-off-by: yuqi <yuqi@datastrato.com>
…verview

Signed-off-by: yuqi <yuqi@datastrato.com>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 13:03

Copilot AI 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.

Warning

Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.

Copilot review overview

4 open findings
What changed in this PR

Implements bounded recent-history replay for the entity change-log poller and adds epoch-fenced cache publication to prevent stale cache entries from surviving invalidations due to commit ordering and publication races.

Changes:

  • Add recent-history replay (window + bounded dedup) with explicit listener opt-in via supportsReplay(), plus diagnostics/metrics/doc updates.
  • Fence cache publication in RelationalEntityStore using an invalidation epoch to prevent raced point-read/insert publications.
  • Update built-in listeners (catalog/entity/jcasbin) and add regression tests covering late commit ordering, replay behavior, and cache publication races.
File Description
server-common/​src/​test/​java/​org/​apache/​gravitino/​server/​authorization/​jcasbin/​TestJcasbinChangePoller.java Adds test ensuring replay opt-in listeners tolerate repeated invalidations.
server-common/​src/​main/​java/​org/​apache/​gravitino/​server/​authorization/​jcasbin/​JcasbinChangeListener.java Opts Jcasbin listener into replay; updates failure-recovery docstring.
docs/​metrics.md Documents replay effects on polling cycles and metrics semantics.
docs/​gravitino-server-config.md Documents new replay configs and operational behavior/limits.
core/​src/​test/​java/​org/​apache/​gravitino/​tag/​TestTagManager.java Mocks new replay config values in tests.
core/​src/​test/​java/​org/​apache/​gravitino/​storage/​relational/​mapper/​provider/​base/​TestEntityChangeLogMapper.java Adds tests for recent replay SQL pagination/clock sources.
core/​src/​test/​java/​org/​apache/​gravitino/​storage/​relational/​TestRelationalEntityStoreHierarchicalCache.java Mocks new replay config values in cache tests.
core/​src/​test/​java/​org/​apache/​gravitino/​storage/​relational/​TestRelationalEntityStore.java Adds regression test for epoch-fenced invalidation behavior with uncached entities.
core/​src/​test/​java/​org/​apache/​gravitino/​storage/​relational/​TestEntityChangeLogPoller.java Adds extensive unit tests for replay, dedup, rate-limiting, and diagnostics logging.
core/​src/​test/​java/​org/​apache/​gravitino/​storage/​relational/​TestCacheCommitOrdering.java Adds JDBC regression test for low-ID late commit reaching peer cache.
core/​src/​test/​java/​org/​apache/​gravitino/​storage/​AbstractEntityStorageTest.java Mocks new replay config values for store tests.
core/​src/​test/​java/​org/​apache/​gravitino/​stats/​TestStatisticManager.java Mocks new replay config values for stats tests.
core/​src/​test/​java/​org/​apache/​gravitino/​policy/​TestPolicyManager.java Mocks new replay config values for policy tests.
core/​src/​test/​java/​org/​apache/​gravitino/​hook/​TestFilesetHookDispatcher.java Mocks new replay config values for hook tests.
core/​src/​test/​java/​org/​apache/​gravitino/​catalog/​TestCatalogWrapperLease.java Validates repeated catalog invalidation safety under active leases.
core/​src/​test/​java/​org/​apache/​gravitino/​catalog/​TestCatalogMutationMarkerRace.java Adds regression probe removing reliance on local mutation markers.
core/​src/​test/​java/​org/​apache/​gravitino/​catalog/​TestCatalogManager.java Updates tests to expect conservative invalidation (including local/replayed rows).
core/​src/​test/​java/​org/​apache/​gravitino/​catalog/​TestCatalogChangeLogListener.java Updates listener tests for “always invalidate catalog” semantics.
core/​src/​test/​java/​org/​apache/​gravitino/​cache/​TestEntityCachePublicationRace.java Adds deterministic tests for cache publication fencing and non-reentrant SPI locks.
core/​src/​test/​java/​org/​apache/​gravitino/​authorization/​TestOwnerManager.java Mocks new replay config values for authorization tests.
core/​src/​test/​java/​org/​apache/​gravitino/​authorization/​TestAccessControlManager.java Mocks new replay config values for access control tests.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​mapper/​provider/​postgresql/​EntityChangeLogPostgreSQLProvider.java Exposes DB clock expression override for replay queries.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​mapper/​provider/​base/​EntityChangeLogBaseSQLProvider.java Adds selectRecentEntityChanges query and DB clock abstraction.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​mapper/​EntityChangeLogSQLProviderFactory.java Adds factory method for recent replay query.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​mapper/​EntityChangeLogMapper.java Adds mapper method for recent replay query.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​RelationalEntityStore.java Adds epoch-fenced publication for put/get/batchGet and avoids re-entrant locking.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​EntityChangeLogPoller.java Implements recent-history replay, bounded dedup, capability gating, and diagnostics.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​EntityChangeLogListener.java Adds supportsReplay() opt-in API with idempotency contract.
core/​src/​main/​java/​org/​apache/​gravitino/​storage/​relational/​EntityCacheChangeLogListener.java Opts entity cache listener into replay.
core/​src/​main/​java/​org/​apache/​gravitino/​catalog/​CatalogManager.java Removes local mutation markers; relies on conservative invalidation + leases.
core/​src/​main/​java/​org/​apache/​gravitino/​catalog/​CatalogChangeLogListener.java Opts into replay and invalidates for all valid catalog events (local/remote/replayed).
core/​src/​main/​java/​org/​apache/​gravitino/​Configs.java Introduces replay window and max-entry config keys with defaults/validation.

🧠 Review effort: Lite


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/metrics.md Outdated
yuqi added 2 commits October 14, 2026 15:38
Catalog operation tests and the entity-store benchmark mock Config, so the
new replay window and dedup capacity entries returned null and
RelationalEntityStore.initialize failed with an NPE.
…lay metrics

Each replay sweep rereads the whole window, so starting one every poll
read every recent row about window/interval (20 by default) times per
server. Start a new sweep at most once per max(pollIntervalSecs,
replayWindowSecs / 4); pages of a sweep in progress and failure retries
are unchanged. Add replay-records-total, replay-failures-total and
dedup-evictions-total so replay health is visible in metrics, not only
in logs.
@github-actions

github-actions Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Code Coverage Report

Overall Project 71.99% +0.28% 🟢
Files changed 86.48% 🟢

Module Coverage
aliyun 19.74% 🔴
api 55.09% 🟢
authorization-common 85.96% 🟢
authorization-ranger 14.44% 🔴
aws 53.54% 🟢
aws-bundle 0.0% 🔴
azure 32.1% 🔴
azure-bundle 0.0% 🔴
catalog-common 29.49% 🔴
catalog-fileset 82.37% 🟢
catalog-glue 73.24% 🟢
catalog-hive 83.63% 🟢
catalog-jdbc-common 57.98% 🟢
catalog-jdbc-doris 87.56% 🟢
catalog-jdbc-mysql 81.91% 🟢
catalog-jdbc-postgresql 84.02% 🟢
catalog-jdbc-starrocks 79.16% 🟢
catalog-kafka 75.21% 🟢
catalog-lakehouse-generic 84.58% 🟢
catalog-lakehouse-hudi 79.1% 🟢
catalog-lakehouse-iceberg 85.93% 🟢
catalog-lakehouse-paimon 84.36% 🟢
catalog-model 77.99% 🟢
cli 44.62% 🟢
client-java 79.44% 🟢
client-java-runtime 0.0% 🔴
common 60.78% 🟢
core 87.53% +0.03% 🟢
filesystem-hadoop3 76.89% 🟢
flink 55.44% 🟢
flink-common 32.2% 🔴
flink-runtime 0.0% 🔴
gcp 32.2% 🔴
hadoop-auth 68.0% 🟢
hadoop-common 17.84% 🔴
hive-metastore-common 62.3% 🟢
hive-metastore3-libs 58.8% 🟢
iceberg-aliyun-bundle 0.0% 🔴
iceberg-common 66.53% 🟢
iceberg-rest-server 77.01% 🟢
idp-basic 86.77% 🟢
integration-test-common 0.0% 🔴
jobs 71.35% 🟢
k8s-job-executor 97.76% 🟢
lance-common 39.9% 🔴
lance-rest-server 69.51% 🟢
lineage 59.39% 🟢
optimizer 84.2% 🟢
optimizer-api 46.58% 🟢
server 91.43% 🟢
server-common 83.86% -0.89% 🟢
spark 61.79% 🟢
tencent 81.78% 🟢
tms 80.0% 🟢
trino-connector 63.31% 🟢
Files
Module File Coverage
core EntityChangeLogMetricsSource.java 100.0% 🟢
EntityChangeLogListener.java 100.0% 🟢
EntityChangeLogBaseSQLProvider.java 100.0% 🟢
EntityChangeLogPostgreSQLProvider.java 100.0% 🟢
Configs.java 99.18% 🟢
CatalogChangeLogListener.java 97.96% 🟢
EntityCacheChangeLogListener.java 97.37% 🟢
EntityChangeLogPoller.java 96.67% 🟢
EntityChangeLogSQLProviderFactory.java 94.12% 🟢
BaseEntityCache.java 92.86% 🟢
RelationalEntityStore.java 91.04% 🟢
CatalogManager.java 76.63% 🟢
SupportsEntityStoreCache.java 0.0% 🔴
EntityChangeLogMapper.java 0.0% 🔴
server-common JcasbinChangeListener.java 63.89% 🟢

@yuqi1129 yuqi1129 self-assigned this Oct 15, 2026
Keep the publication epoch sample in put() alongside the lock-wait
timeout mapping from apache#13792, and stub the replay settings in the new
TestRelationalEntityStoreLockWaitTimeout mocked Config.
@yuqi1129 yuqi1129 changed the title [#13828] fix(cache): Recover late change logs and fence cache publication [#13893] fix(cache): Recover late change logs and fence cache publication Oct 16, 2026

@jerryshao jerryshao 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.

Verdict: ship it

Reviewed the full change on e791a8f (39 files, +1873/-279) in a local checkout of pull/13831/head, reading every changed main-source file end to end plus BaseEntityCache, CaffeineEntityCache, SegmentedLock, the three listener implementations, the change-log schema scripts, and the new tests. No correctness defect survived verification: the replay keyset query is sound (moving the window lower bound forward during a multi-page sweep can only drop rows already paged past, and the (created_at, id) tiebreak plus the fixed upperId ceiling keep pages stable), the dedup lifetime argument in discussion_r4291682643 holds for any fixed clock offset (an entry is inserted at or after the row's created_at and expires one window later, so it always outlives the row's presence in the query), and all three registered listeners are idempotent, so the replay opt-in is safe. The markLocalMutation removal leaves no stale references anywhere in the tree, and the CatalogWrapper lease/retire protocol does cover the NPE risk the old marker comment described, since cleanup is claimed only when retired && activeOps == 0.

The two Important items below are efficiency/effectiveness regressions, not correctness blockers, so they are the author's call to fix here or in a follow-up.

Tests: adequate for the behaviour being fixed. TestCacheCommitOrdering reproduces the real low-id-late-commit race against H2/MySQL/PostgreSQL, TestEntityChangeLogPoller covers the throttle floor, dedup expiry/eviction, replay failure isolation and rate limiting, the opt-in gate and the capability-check failure path, and TestEntityCachePublicationRace covers the fencing. Two gaps worth a line each: nothing asserts the query volume of a multi-page replay-only drain (the drain tests at TestEntityChangeLogPoller.java:658 and :698 only exercise incremental drains), which is the first finding below; and nothing exercises a mid-sweep replay page failure followed by resumption from the saved page cursor, although the saved-cursor code path is simple enough to read.

No findings had to be dropped for size.

Review drafted with Claude, requested by @jerryshao.

e);
}
}
return incrementalBacklog || replayBacklog;

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.

[Important] A full replay page makes this return true, so runPollCycle sets nextDelayMillis = 0 (:322) and the next cycle starts at once. But fetchNextDelivery samples the database tail whenever changes.size() < batchSize (:581-583), and during a replay-only drain the incremental query is empty, so that first disjunct is always true. The once-per-interval guard whose comment at :584-585 says "Sample at most once per interval during a drain" therefore never applies to a replay-driven drain: every zero-delay cycle issues three queries back to back (incremental, tail, replay). A sweep that needs more than one page -- window rows above pollBatchSize, 2000 by default, or any cold-start sweep over a busy window -- runs that triple at full speed with no delay between cycles, which is exactly the query amplification the guard was added to bound.

The comment at :320-322 and metrics.md both note the three queries, so this is clearly known; noting the cost is not the same as wanting it. Suggest extending the existing rate limit to cover immediately-rescheduled cycles, e.g. skip the tail sample when the previous cycle returned a backlog and nowNanos - lastTailSampleNanos < pollIntervalSecs, regardless of which of the two queries was full. The tail is observability-only (:577-578), so a 15s-stale gauge during a drain costs nothing.

Verified by: reading doPollChanges (:372-408), runPollCycle (:313-325) and fetchNextDelivery (:574-600) at this head, and confirming no test asserts tail-query count for a replay drain.

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. You are right that the interval guard does not cover a replay-only drain. I'm keeping it as-is for this PR, for two reasons:

  • Cost: the extra query is SELECT COALESCE(MAX(id), 0), which is a single primary-key index lookup. That is negligible next to the replay page query it runs beside.
  • Count: it is bounded by the pages in one sweep, at most windowRows / pollBatchSize - 1 extra calls per sweep. For example, at 100 writes/s the window holds about 6,000 rows, which means about 2 extra MAX(id) calls every 15 s.

Fixing it would add one more piece of "previous cycle had a backlog" state to a poller that already carries a lot of state, and this PR aims to stay a short-term fix. If the tail query ever shows up in profiles, the rate limit you suggest is the right follow-up. The extra query is already documented (metrics.md and the comment at :320-322).

@Override
public <E extends Entity & HasIdentifier> void put(E e, boolean overwritten)
throws IOException, EntityAlreadyExistsException {
long epochBeforeWrite = cacheInvalidationEpoch.get();

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.

[Important] This samples the fence before backend.insert, so the publication of e is discarded if the epoch moved at any point during the whole write round trip. cacheInvalidationEpoch is a single process-wide counter (:100) incremented by every invalidateCache (:693) -- once per local put/update/delete, and once per change-log row delivered from another node, because the poller's target routes through this same method (newCacheChangeLogListener, :173-188). So an invalidation of a completely unrelated entity suppresses the caching of a value this node just wrote and knows to be current, and a poller batch of N rows bumps the counter N times, so any write (or point read, :273-276) overlapping a delivery burst loses its cache fill. Before this PR both put and get published unconditionally, so this is a new cost on cache fill rate, paid under exactly the write load where the cache matters most.

The same-key race the fence targets is already excluded here by the key lock taken in cacheIfEpochUnchanged plus the post-put recheck in publishIfEpochUnchanged (:680-686): a same-key delete that commits before this insert has already bumped the epoch before the sample, and one that commits after will invalidate the entry once it gets the key lock. So the pre-write sample buys nothing for put that a sample taken after backend.insert would not.

Two options worth considering: sample the epoch after the insert for put, or make the fence per key (a per-key invalidation version, or a per-segment counter) so unrelated invalidations stop suppressing publication on all three paths. I have not measured the hit-rate impact, so the magnitude is open -- but the mechanism is unconditional.

Verified by: reading put (:228-245), the epoch field (:100), invalidateCache (:690-695) and newCacheChangeLogListener (:173-188) at this head.

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 for the careful analysis. The cost you describe is real. A fill is skipped on any epoch change, and this was an accepted trade-off: a skipped fill only means the next read goes to the database. The pre-insert sample is still deliberate, though. A post-insert sample reopens the ancestor race. CaffeineEntityCache.invalidate takes only the ancestor key's segment lock and scans an index snapshot; it never takes the child's lock:

step T1: put(table t) T2: drop schema s (cascade)
1 insert t holding FOR SHARE on s, commit waits for FOR UPDATE on s
2 cascade-deletes t, commits
3 epoch++, invalidates s and its descendants; t is not cached yet
4 post-insert sample reads the new epoch
5 takes t's key lock, sees an unchanged epoch, publishes t

The dropped t then stays cached until TTL. That is the stale-publication case #13828 P0-2 targets. With the sample taken before the insert, step 5 sees a changed epoch and skips. The key lock and the post-put recheck only exclude a same-key invalidation, not an ancestor invalidation that has already finished.

A per-key fence would remove the false skips. One option is striped counters checked along the ancestor chain, with clears still bumping the global epoch. That is a larger change than this P0 needs, so I'd rather measure the hit-rate impact first, as part of the #13828 performance work (item 6). Hit rate is the next step there.

Comment on lines +65 to +67
@Override
protected String currentTimeMillisSql() {
return CURRENT_TIME_MILLIS_SQL;

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.

[Question] This override points the replay window filter at CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) * 1000 AS BIGINT) (DatabaseTimeSQL.java:43-44), and insertEntityChange stamps created_at with the same expression (:35, :45-50). On PostgreSQL CURRENT_TIMESTAMP is the transaction start time, so a transaction that begins, writes its change-log row, and commits 51s later records created_at at its start: by the time the row becomes visible its recorded age already exceeds replayWindowSecs - sweepInterval (45s with defaults) and replay never sees it. That is precisely the long-transaction late-commit case this PR exists to fix, and PostgreSQL is the only backend with this semantics -- MySQL/H2 CURRENT_TIMESTAMP is statement time, so there the window behaves as documented.

The PR description and docs/gravitino-server-config.md both call this out, so it is a known limitation rather than an oversight. Was clock_timestamp() (statement time) considered for the PostgreSQL insert path? It would restore the full window for long transactions. If it was rejected because pruneOldEntityChanges (:51-62) and retentionSecs would then be measured from statement time instead, saying so in the doc paragraph would help operators who are sizing the window against their transaction durations.

Verified by: reading the PostgreSQL provider in full, DatabaseTimeSQL (:42-45), and the new selectRecentEntityChanges filter (EntityChangeLogBaseSQLProvider.java:72-88).

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.

Yes, it was considered. clock_timestamp() on the PostgreSQL insert would make created_at statement time, matching MySQL/H2. Since EntityChangeLogWriter.append runs right before commit, that would almost remove the long-transaction gap.

I'm keeping the existing writer in this PR on purpose:

  • Scope: the gap only matters for PostgreSQL transactions that take longer than the window minus one sweep (45 s by default) from start to commit. Normal metadata transactions take milliseconds to seconds, so the ordering problem this PR fixes is covered at the default window.
  • Mitigation: the limitation is documented (PR description and gravitino-server-config.md), and replayWindowSecs can be raised, up to 86400.
  • Risk: changing how created_at is stamped on PostgreSQL is a separate behavior change that needs its own PostgreSQL validation.

If long transactions turn out to lose invalidations in practice, switching the PostgreSQL insert to clock_timestamp() is a small follow-up, and retention would not be affected.

…n and startup replay logging

- Document that invalidateOnKeyChange may run under the entity's own cache
  lock and must not take another entity's lock.
- Drop the duplicated non-cacheable branch in batchGet; the publication
  helper already handles it.
- Log the first replay sweep after startup at INFO as startup history and
  keep the late-commit WARN for later sweeps.
@yuqi1129

Copy link
Copy Markdown
Contributor Author

@jerryshao thanks for the thorough review. I replied to each inline comment. Summary:

  • Fixed:

    • The invalidateOnKeyChange lock contract is now documented.
    • The duplicated batchGet branch is removed.
    • The first replay sweep after startup logs at INFO instead of WARN.

    All three are in 5a1312f.

  • Kept, with reasons in each thread:

    • The tail query during a replay drain: a single primary-key lookup, bounded by the pages in one sweep.
    • The pre-insert epoch sample: a post-insert sample reopens the ancestor-invalidation race; the timeline is in the thread. A per-key fence will be evaluated with hit-rate data under [EPIC] Cache correctness and performance improvements #13828.
    • PostgreSQL transaction-start created_at: a documented limitation; clock_timestamp() is a possible small follow-up.

On the two test gaps:

  • Mid-sweep replay page failure: covered by testFailedReplayPageResumesFromSavedPageCursor in 6a242d6. It checks that the failed page is retried from the saved (created_at, id) cursor with the same ceiling, and that the sweep does not restart. The test fails if the failure path resets the cursor.
  • Query volume of a replay-only drain: not added, because that behavior is intentionally unchanged (see the reply on doPollChanges).

@jerryshao
jerryshao merged commit 28fd283 into apache:main Oct 16, 2026
43 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Subtask] Fix P0 cache invalidation gaps: late change-log commits, stale cache publication, catalog local markers

3 participants