[#12440] improvement(core): Replace the entity change log listener retry/EXIT policy with a cache-clear fallback - #12445
Conversation
…XIT policy with local recovery Dispatch each entity change log batch once and always advance the cursor: listeners are self-healing, so retrying a batch for a failing listener only blocks cache invalidation for every other listener in the process, and EXIT trades the whole server for a condition a local cache clear already fixes. Removes gravitino.entityChangeLog.listenerMaxRetries and gravitino.entityChangeLog.listenerFailureAction (both VERSION_2_0_0 and unreleased), along with ListenerFailureAction, exitHandler, pendingDelivery, BatchDelivery.retryOnly/attempts and handleExhaustedRetries. Listener failures are now logged at ERROR instead of WARN.
…lf-healing too JcasbinChangeListener is a third entity change log listener that apache#12440 did not account for. It tolerated poison rows but propagated a failed invalidation, so under the dispatch-once contract its metadataIdCache would serve a stale name->id mapping to authorization decisions until the entry's TTL expired. It now clears metadataIdCache on a failed invalidation, the same derived-state recovery EntityCacheChangeLogListener performs. ownerRelCache is untouched: it is driven by this listener's own owner_meta poller, whose cursor only advances after a successful invalidation batch, so it already retries. The version-validated caches (userRoleCache, groupRoleCache, loadedRoles) probe the DB on every read and cannot go stale from a missed batch. Also corrects the EntityChangeLogListener interface javadoc, which still documented the removed retry contract.
…that failed Under the dispatch-once contract, a swallowed catalog eviction leaves this node serving that catalog stale for a full eviction interval. The listener now retries the single failed eviction before giving up, and escalates a persistent failure to ERROR. Recovery stays scoped to the identifier the change log record named. Clearing the whole catalog cache, the fallback the entity and JCasbin caches use, would evict catalogs this process is actively serving and close their in-use IsolatedClassLoaders (apache#11739). The retry also never re-runs the single-shot consumeLocalMutation probe, which would classify a local mutation as remote and tear down a catalog this node just mutated itself.
… eviction Per review decision, all three change log listeners now recover uniformly: a failed eviction clears the whole catalog cache, a strict superset of the eviction that failed and of the rest of the batch, so a changed catalog is never served stale. Tradeoff accepted deliberately and documented in the class javadoc: clearing closes the CatalogWrapper of every cached catalog, including catalogs this process is actively serving, so in-flight requests can hit NoClassDefFoundError from a closed IsolatedClassLoader (apache#11739). The clear runs only on a failed eviction, off the normal path. Malformed rows and a failed consumeLocalMutation probe are still skipped rather than escalated: they name no eviction to recover, so clearing there would tear down every cached catalog over a bookkeeping failure. A failed clear now propagates to the poller, which logs it and advances, matching the other two listeners.
There was a problem hiding this comment.
Pull request overview
This pull request changes the EntityChangeLogPoller contract to “dispatch once and always advance the cursor”, removes the retry/EXIT machinery and related configs, and shifts resilience to each listener via local self-healing (cache-clear fallbacks) so one failing listener can’t stall process-wide cache coherence.
Changes:
- Simplify
EntityChangeLogPollerto deliver each batch once, log listener failures atERROR, and always advance the cursor; remove retry/pause/EXIT/SKIP behavior and associated config keys/wiring. - Update listeners (notably
JcasbinChangeListenerandCatalogChangeLogListener) to recover from invalidation failures by clearing their maintained caches, and update listener/poller JavaDoc accordingly. - Update documentation and expand/adjust unit tests and test config stubs to reflect the new contract and removed configuration.
Reviewed changes
Copilot reviewed 26 out of 26 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| server-common/src/test/java/org/apache/gravitino/server/authorization/jcasbin/TestJcasbinChangePoller.java | Expands unit coverage for metadataIdCache invalidation behavior and cache-clear fallback scenarios. |
| server-common/src/main/java/org/apache/gravitino/server/authorization/jcasbin/JcasbinChangeListener.java | Adds “clear whole cache” fallback on invalidation failure under the dispatch-once contract and updates JavaDoc. |
| docs/gravitino-server-config.md | Removes the deleted listener retry/failure-action configuration entries from server config docs. |
| core/src/test/java/org/apache/gravitino/tag/TestTagManager.java | Removes stubs/imports for deleted entity-change-log listener retry/failure-action configs. |
| core/src/test/java/org/apache/gravitino/storage/relational/TestRelationalEntityStoreHierarchicalCache.java | Removes stubs for deleted entity-change-log listener retry/failure-action configs. |
| core/src/test/java/org/apache/gravitino/storage/relational/TestEntityChangeLogPoller.java | Reworks tests to assert dispatch-once behavior, cursor advancement, and unregister skipping semantics. |
| core/src/test/java/org/apache/gravitino/storage/relational/TestEntityCacheCrossNodeInvalidation.java | Updates poller construction to match new constructor/signature (no retry/failure-action args). |
| core/src/test/java/org/apache/gravitino/storage/AbstractEntityStorageTest.java | Removes stubs/imports for deleted entity-change-log listener retry/failure-action configs. |
| core/src/test/java/org/apache/gravitino/stats/TestStatisticManager.java | Removes stubs/imports for deleted entity-change-log listener retry/failure-action configs. |
| core/src/test/java/org/apache/gravitino/policy/TestPolicyManager.java | Removes stubs/imports for deleted entity-change-log listener retry/failure-action configs. |
| core/src/test/java/org/apache/gravitino/hook/TestFilesetHookDispatcher.java | Removes stubs/imports for deleted entity-change-log listener retry/failure-action configs. |
| core/src/test/java/org/apache/gravitino/catalog/TestCatalogChangeLogListener.java | Expands tests for catalog-cache clear fallback and propagation behavior under dispatch-once semantics. |
| core/src/test/java/org/apache/gravitino/authorization/TestOwnerManager.java | Removes stubs/imports for deleted entity-change-log listener retry/failure-action configs. |
| core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java | Removes stubs/imports for deleted entity-change-log listener retry/failure-action configs. |
| core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java | Removes wiring for deleted listener retry/failure-action configs when constructing the poller. |
| core/src/main/java/org/apache/gravitino/storage/relational/EntityChangeLogPoller.java | Implements dispatch-once + unconditional cursor advancement; removes retry/EXIT machinery and related state. |
| core/src/main/java/org/apache/gravitino/storage/relational/EntityChangeLogListener.java | Updates listener contract JavaDoc to require self-healing (batch is never replayed). |
| core/src/main/java/org/apache/gravitino/storage/relational/EntityChangeLogCleaner.java | Updates retention rationale comment to reflect backlog/slow-drain rather than paused-retry semantics. |
| core/src/main/java/org/apache/gravitino/storage/relational/EntityCacheChangeLogListener.java | Updates JavaDoc to reflect that poller logs and moves on if listener recovery ultimately fails (no retries). |
| core/src/main/java/org/apache/gravitino/Configs.java | Removes deleted config entries/constants and associated validation for listener retry/failure-action settings. |
| core/src/main/java/org/apache/gravitino/catalog/CatalogChangeLogListener.java | Changes catalog listener to clear whole catalog cache on eviction failure and documents the classloader tradeoff. |
| core/src/jmh/java/org/apache/gravitino/cache/it/AbstractEntityStorageBenchmark.java | Removes benchmark config stubs for deleted listener retry/failure-action configs. |
| catalogs/catalog-model/src/test/java/org/apache/gravtitino/catalog/model/TestModelCatalogOperations.java | Removes test config stubs for deleted listener retry/failure-action configs. |
| catalogs/catalog-lakehouse-generic/src/test/java/org/apache/gravitino/catalog/lakehouse/generic/TestGenericCatalogOperations.java | Removes test config stubs for deleted listener retry/failure-action configs. |
| catalogs/catalog-kafka/src/test/java/org/apache/gravitino/catalog/kafka/TestKafkaCatalogOperations.java | Removes test config stubs for deleted listener retry/failure-action configs. |
| catalogs/catalog-fileset/src/test/java/org/apache/gravitino/catalog/fileset/TestFilesetCatalogOperations.java | Removes test config stubs for deleted listener retry/failure-action configs. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 26 out of 26 changed files in this pull request and generated no new comments.
Suppressed comments (1)
core/src/main/java/org/apache/gravitino/storage/relational/EntityCacheChangeLogListener.java:57
- The Javadoc says stale entity-cache entries may be served "until they expire", but cache expiration can be disabled (e.g. TTL set to 0), in which case entries may remain stale indefinitely. Consider rewording to avoid implying expiry is guaranteed.
* clear fails the exception propagates to {@link EntityChangeLogPoller}, which logs it and
* moves on: the batch is not retried, so this node may keep serving stale entries until they
* expire.
Code Coverage Report
Files
|
| * on access, so staleness is bounded by {@code gravitino.catalog.cache.evictionIntervalMs}. | ||
| * <p>The poller requires each listener to be self-healing, and this one recovers the same way | ||
| * {@code EntityCacheChangeLogListener} and {@code JcasbinChangeListener} do: a failed eviction | ||
| * clears the whole catalog cache, which is a strict superset of the eviction that failed and of the |
There was a problem hiding this comment.
What's the meaning here "that failed and of the"?
| "Invalidating catalog cache for {} due to a remote {} recorded in change log id {}", | ||
| localMutation = catalogManager.consumeLocalMutation(ident); | ||
| } catch (RuntimeException e) { | ||
| // The identifier is valid, so this record may name a remote mutation. Treating an unknown |
There was a problem hiding this comment.
What's the meaning of "name a remote mutation"? The comment is really hard to understand.
| * <p>The poller requires each listener to be self-healing, and this one recovers the same way | ||
| * {@code EntityCacheChangeLogListener} and {@code JcasbinChangeListener} do: a failed eviction | ||
| * clears the whole catalog cache, which is a strict superset of the eviction that failed and of the | ||
| * rest of the batch. A malformed row is skipped instead, because it names no catalog and so leaves |
There was a problem hiding this comment.
What is the meaning of "names no catalog"?
|
Most of the comments are too vague to understand. You don't have to explain everything, just make clear about your changes, that should be enough. |
jerryshao
left a comment
There was a problem hiding this comment.
A couple of correctness/robustness observations from a deeper pass on the self-healing contract this PR introduces.
| @@ -410,39 +307,29 @@ private List<EntityChangeLogListener> notifyListeners(BatchDelivery delivery) { | |||
| delivery.firstChangeId(), | |||
| delivery.lastChangeId); | |||
| } catch (Exception e) { | |||
There was a problem hiding this comment.
catch (Exception e) here (and in pollChanges() at line 176) only catches Exception, not Error. scheduleWithFixedDelay's contract is that any uncaught Throwable from a periodic task silently and permanently cancels all future executions. Notably, CatalogChangeLogListener's javadoc in this same PR names NoClassDefFoundError as an accepted failure mode of its whole-cache-clear fallback (the #11739 failure mode) — so a listener can throw an Error under exactly the recovery path this PR introduces, which would kill the poller thread for every listener, not just the failing one. That contradicts the "self-healing" contract this class's own javadoc asserts (listener failures are always logged and the cursor always advances). Worth catching Throwable here, or documenting why Error is intentionally out of scope?
| continue; | ||
| } | ||
|
|
||
| Optional<NameIdentifier> identOpt = catalogIdentifier(change); |
There was a problem hiding this comment.
catalogIdentifier(change) is called here with no enclosing try/catch. The old code wrapped identifier resolution + consumeLocalMutation + invalidate for a record in one catch (RuntimeException), isolating any unexpected failure to a single row. Today this is safe because catalogIdentifier() only catches IllegalArgumentException internally, but that safety now depends entirely on an implementation detail of decode()/NameIdentifier.of() two calls down, with no defensive boundary at this call site. If a future change to that codec throws a different unchecked exception, onEntityChange() would abort for the whole batch — skipping the self-heal cache-clear for every already-collected remoteInvalidations — rather than being isolated to one bad row, as the class javadoc claims ("a malformed row is skipped ... and leaves nothing stale"). Worth wrapping this call (or the loop body) defensively?
What changes were proposed in this pull request?
Make "dispatch once, always advance the cursor, listeners are self-healing" the entity change log contract, and let each listener recover locally.
Poller and configs:
EntityChangeLogPoller: removeListenerFailureAction,exitHandler,pendingDelivery,BatchDelivery.retryOnly/attemptsandhandleExhaustedRetries. Each batch is dispatched once and the cursor always advances; every listener failure is logged atERROR. The self-healing contract is stated in the poller andEntityChangeLogListenerjavadoc, so a listener that cannot contain its own failures is not added silently.gravitino.entityChangeLog.listenerMaxRetriesandgravitino.entityChangeLog.listenerFailureActionfromConfigs, their wiring inRelationalEntityStore, and their entries indocs/gravitino-server-config.md.All three registered listeners now recover by clearing the cache they maintain, which is a strict superset of the invalidation that failed and of the rest of the batch:
EntityCacheChangeLogListenerJcasbinChangeListenermetadataIdCacheCatalogChangeLogListenerNotes on the two listeners that changed:
JcasbinChangeListeneris a third change-log listener that the issue did not account for. It tolerated poison rows but propagated a failed invalidation, so under the dispatch-once contract itsmetadataIdCachewould feed a stale name→id mapping to authorization decisions until the entry's TTL expired.CatalogChangeLogListenerclears the catalog cache on a failed eviction. This is a deliberate tradeoff, documented in its javadoc: clearing closes theCatalogWrapperof every cached catalog, including catalogs this process is actively serving, so in-flight requests can hitNoClassDefFoundErrorfrom a closedIsolatedClassLoader(the failure mode of [#11736] fix(core): retry failed entity change log listeners and do not remove logs when polling. #11739). It is accepted so that a changed catalog is never served stale, and the clear runs only on a failed eviction, off the normal path. Malformed rows and a failedconsumeLocalMutationprobe are still skipped rather than escalated, since they name no eviction to recover.Caches deliberately left alone:
ownerRelCacheis driven byJcasbinChangeListener's ownowner_metapoller, whose cursor only advances after a successful invalidation batch, so it already retries;userRoleCache,groupRoleCacheandloadedRolesare version-validated on every read and cannot go stale from a missed batch.Why are the changes needed?
After #12374 every registered listener can recover locally, so the retry/
EXITpath is effectively unreachable while carrying real cost:EXITtrades the whole server for a condition a local cache clear already resolves. Killing a node to fix a stale cache entry is a heavy, surprising failure mode for operators.pendingDelivery,BatchDelivery.retryOnly,attemptstracking andhandleExhaustedRetriesadd machinery and two public configs for a path no listener reaches.Fix: #12440
Does this PR introduce any user-facing change?
Yes:
gravitino.entityChangeLog.listenerMaxRetriesandgravitino.entityChangeLog.listenerFailureAction. Both areVERSION_2_0_0and 2.0.0 is unreleased, so no deprecation cycle is needed.System.exit(1)) when a listener keeps failing to apply a change log batch.How was this patch tested?
New and reworked unit tests:
TestEntityChangeLogPoller: the four retry/pause/EXIT/SKIP cases are replaced bytestThrowingListenerNeitherPausesCursorNorBlocksOtherListeners(each batch dispatched exactly once, the healthy listener sees every batch, the cursor advances past both) andtestUnregisteredListenerIsSkipped.TestJcasbinChangePoller(7 → 14): themetadataIdCacheclear fallback on prefix, leaf-key and batch-lock failures; a failed clear propagating to the poller; the happy path clearing nothing;ownerRelCachenot cleared as collateral; plus leaf-vs-prefix keying, which had no coverage.TestCatalogChangeLogListener(3 → 7): the clear on a failed eviction; no clear on the happy path; malformed rows and a failedconsumeLocalMutationprobe skipped without clearing; a failed clear propagating.Suites run locally:
:core:test(1657 tests) and:server-common:test(272 tests) with--rerun-tasks, both green, plus the unit tests of the four catalog modules whose config stubs changed.:core:javadocreports no new warnings on the touched files. Docker was not available locally, so docker-tagged tests and integration tests were not run.