-
Notifications
You must be signed in to change notification settings - Fork 906
[#12440] improvement(core): Replace the entity change log listener retry/EXIT policy with a cache-clear fallback #12445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
cd1f08a
7177a51
ad00a7a
0682f9a
c464c2e
ad98b33
71d2445
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,6 +18,7 @@ | |
| */ | ||
| package org.apache.gravitino.catalog; | ||
|
|
||
| import java.util.ArrayList; | ||
| import java.util.List; | ||
| import java.util.Locale; | ||
| import java.util.Optional; | ||
|
|
@@ -35,12 +36,24 @@ | |
| * <p>This listener is called <em>synchronously</em> in the poller thread. Implementations must not | ||
| * block or perform expensive I/O; only fast, in-memory cache invalidations are permitted. | ||
| * | ||
| * <p>This listener never propagates a failure to the poller, so the poller never retries a batch | ||
| * for it. That is deliberate: local-mutation de-duplication ({@link | ||
| * CatalogManager#consumeLocalMutation}) is single-shot, so re-delivering an already-applied batch | ||
| * would invalidate a catalog this process mutated itself and close its still-in-use {@code | ||
| * IsolatedClassLoader}. Dropping an invalidation is the cheaper failure: the catalog cache expires | ||
| * 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 | ||
| * rest of the batch. A malformed row is skipped instead, because it names no catalog and so leaves | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the meaning of "names no catalog"? |
||
| * nothing stale. | ||
| * | ||
| * <p>The listener consumes local-mutation markers for the whole batch before evicting anything. A | ||
| * failed eviction or clear therefore cannot strand a marker that would make a later remote change | ||
| * look local. If the clear itself fails, the exception reaches the poller, which logs it at {@code | ||
| * ERROR} and advances its cursor; the affected catalog can then remain stale until it expires. | ||
| * | ||
| * <p><b>Cost of the clear:</b> evicting a cached catalog closes its {@code CatalogWrapper}, which | ||
| * tears down the connection pool and the {@code IsolatedClassLoader}. A whole-cache clear therefore | ||
| * also closes catalogs this process is actively serving; requests holding classes from a closed | ||
| * loader can fail with {@code NoClassDefFoundError}, the failure mode of #11739. This is accepted | ||
| * deliberately so that a stale catalog is never served: the alternative left this node serving the | ||
| * changed catalog from cache for up to {@code gravitino.catalog.cache.evictionIntervalMs}. The | ||
| * clear runs only on a failed eviction, which is off the normal path. | ||
| */ | ||
| public class CatalogChangeLogListener implements EntityChangeLogListener { | ||
|
|
||
|
|
@@ -59,51 +72,87 @@ public CatalogChangeLogListener(CatalogManager catalogManager) { | |
|
|
||
| @Override | ||
| public void onEntityChange(List<EntityChangeRecord> changes) { | ||
| List<CatalogInvalidation> remoteInvalidations = new ArrayList<>(); | ||
| for (EntityChangeRecord change : changes) { | ||
| if (!isCatalogChange(change)) { | ||
| continue; | ||
| } | ||
|
|
||
| Optional<NameIdentifier> identOpt = catalogIdentifier(change); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| if (identOpt.isEmpty()) { | ||
| // Already logged. A row that names no catalog cannot leave a stale entry behind, so it is | ||
| // skipped rather than escalated to a cache clear. | ||
| continue; | ||
| } | ||
| NameIdentifier ident = identOpt.get(); | ||
|
|
||
| boolean localMutation; | ||
| try { | ||
| if (!isCatalogChange(change)) { | ||
| continue; | ||
| } | ||
|
|
||
| Optional<NameIdentifier> identOpt = catalogIdentifier(change); | ||
| if (identOpt.isEmpty()) { | ||
| continue; | ||
| } | ||
| NameIdentifier ident = identOpt.get(); | ||
|
|
||
| if (catalogManager.consumeLocalMutation(ident)) { | ||
| LOG.debug( | ||
| "Skipping catalog cache invalidation for local mutation: {}, change log id {}", | ||
| ident, | ||
| change.getId()); | ||
| continue; | ||
| } | ||
|
|
||
| // Logged at INFO on purpose: this tears down the cached catalog, including its connection | ||
| // pool and isolated classloader, and it is the main cross-node effect of the change log. | ||
| // CatalogManager logs the matching "Closing catalog" line when the eviction runs. | ||
| LOG.info( | ||
| "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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the meaning of "name a remote mutation"? The comment is really hard to understand. |
||
| // origin as remote can cause an unnecessary eviction, but skipping it can leave a stale | ||
| // catalog cached after the poller advances its cursor. | ||
| LOG.error( | ||
| "Failed to check local mutation state for catalog {}, treating change log record id {} " | ||
| + "as remote to avoid serving stale metadata", | ||
| ident, | ||
| change.getId(), | ||
| e); | ||
| localMutation = false; | ||
| } | ||
|
|
||
| if (localMutation) { | ||
| LOG.debug( | ||
| "Skipping catalog cache invalidation for local mutation: {}, change log id {}", | ||
| ident, | ||
| change.getOperateType(), | ||
| change.getId()); | ||
| continue; | ||
| } | ||
|
|
||
| remoteInvalidations.add(new CatalogInvalidation(change, ident)); | ||
| } | ||
|
|
||
| for (CatalogInvalidation invalidation : remoteInvalidations) { | ||
| EntityChangeRecord change = invalidation.change; | ||
| NameIdentifier ident = invalidation.ident; | ||
| // Logged at INFO on purpose: this tears down the cached catalog, including its connection | ||
| // pool and isolated classloader, and it is the main cross-node effect of the change log. | ||
| // CatalogManager logs the matching "Closing catalog" line when the eviction runs. | ||
| LOG.info( | ||
| "Invalidating catalog cache for {} due to a remote {} recorded in change log id {}", | ||
| ident, | ||
| change.getOperateType(), | ||
| change.getId()); | ||
|
|
||
| try { | ||
| catalogManager.getCatalogCache().invalidate(ident); | ||
| } catch (RuntimeException e) { | ||
| // Deliberately not rethrown: see the class javadoc. A dropped invalidation only costs | ||
| // bounded staleness here, while a retry of an already-applied batch can tear down a | ||
| // catalog that is still in use. | ||
| LOG.warn( | ||
| "Failed to process catalog change log record: id={}, fullName={}, entityType={}, " | ||
| + "operateType={}", | ||
| // The poller dispatches a batch once and never replays it, so dropping this eviction would | ||
| // serve the catalog stale until the eviction interval expires. The whole cache is cleared | ||
| // instead; see the class javadoc for the classloader cost this accepts. | ||
| LOG.error( | ||
| "Failed to evict catalog {} for change log id {}, clearing the whole catalog cache to " | ||
| + "avoid serving it stale; catalogs in use by this node are closed as a result", | ||
| ident, | ||
| change.getId(), | ||
| change.getFullName(), | ||
| change.getEntityType(), | ||
| change.getOperateType(), | ||
| e); | ||
| catalogManager.getCatalogCache().invalidateAll(); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private static class CatalogInvalidation { | ||
| private final EntityChangeRecord change; | ||
| private final NameIdentifier ident; | ||
|
|
||
| private CatalogInvalidation(EntityChangeRecord change, NameIdentifier ident) { | ||
| this.change = change; | ||
| this.ident = ident; | ||
| } | ||
| } | ||
|
|
||
| private boolean isCatalogChange(EntityChangeRecord change) { | ||
| if (change.getEntityType() == null) { | ||
| return false; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the meaning here "that failed and of the"?