-
Notifications
You must be signed in to change notification settings - Fork 803
[#10739] fix(core): Avoid stale cache refill during concurrent metadata updates #10740
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 1 commit
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 | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -27,6 +27,7 @@ | |||||||||||
| import static org.apache.gravitino.TestCatalog.PROPERTY_KEY6_PREFIX; | ||||||||||||
| import static org.awaitility.Awaitility.await; | ||||||||||||
| import static org.mockito.ArgumentMatchers.any; | ||||||||||||
| import static org.mockito.ArgumentMatchers.eq; | ||||||||||||
|
|
||||||||||||
| import com.google.common.collect.ImmutableMap; | ||||||||||||
| import com.google.common.collect.Maps; | ||||||||||||
|
|
@@ -36,6 +37,7 @@ | |||||||||||
| import java.time.Instant; | ||||||||||||
| import java.util.Map; | ||||||||||||
| import java.util.Set; | ||||||||||||
| import java.util.concurrent.atomic.AtomicBoolean; | ||||||||||||
| import org.apache.commons.lang3.reflect.FieldUtils; | ||||||||||||
| import org.apache.gravitino.Catalog; | ||||||||||||
| import org.apache.gravitino.CatalogChange; | ||||||||||||
|
|
@@ -69,6 +71,7 @@ | |||||||||||
| import org.junit.jupiter.api.BeforeEach; | ||||||||||||
| import org.junit.jupiter.api.Test; | ||||||||||||
| import org.mockito.Mockito; | ||||||||||||
| import org.mockito.stubbing.Answer; | ||||||||||||
|
|
||||||||||||
| public class TestCatalogManager { | ||||||||||||
|
|
||||||||||||
|
|
@@ -540,8 +543,8 @@ public void testAlterCatalog() { | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| @Test | ||||||||||||
| public void testDropCatalog() throws Exception { | ||||||||||||
| NameIdentifier ident = NameIdentifier.of("metalake", "test41"); | ||||||||||||
| void testAlterCatalogRefreshesCacheAfterStoreUpdate() throws Exception { | ||||||||||||
| NameIdentifier ident = NameIdentifier.of("metalake", "cache_race_test"); | ||||||||||||
| Map<String, String> props = | ||||||||||||
| ImmutableMap.of( | ||||||||||||
| "provider", | ||||||||||||
|
|
@@ -552,46 +555,63 @@ public void testDropCatalog() throws Exception { | |||||||||||
| "value2", | ||||||||||||
| PROPERTY_KEY5_PREFIX + "1", | ||||||||||||
| "value3"); | ||||||||||||
| String comment = "comment"; | ||||||||||||
|
|
||||||||||||
| Catalog catalog = | ||||||||||||
| catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, comment, props); | ||||||||||||
|
|
||||||||||||
| // Test drop catalog | ||||||||||||
| Exception exception = | ||||||||||||
| Assertions.assertThrows( | ||||||||||||
| CatalogInUseException.class, () -> catalogManager.dropCatalog(ident)); | ||||||||||||
| Assertions.assertTrue(exception.getMessage().contains("Catalog metalake.test41 is in use")); | ||||||||||||
|
|
||||||||||||
| Assertions.assertDoesNotThrow(() -> catalogManager.disableCatalog(ident)); | ||||||||||||
|
|
||||||||||||
| CatalogEntity oldEntity = entityStore.get(ident, EntityType.CATALOG, CatalogEntity.class); | ||||||||||||
| FieldUtils.writeField(catalog, "entity", oldEntity, true); | ||||||||||||
|
|
||||||||||||
| CatalogManager.CatalogWrapper catalogWrapper = | ||||||||||||
| Mockito.mock(CatalogManager.CatalogWrapper.class); | ||||||||||||
| Capability capability = Mockito.mock(Capability.class); | ||||||||||||
| CapabilityResult unsupportedResult = CapabilityResult.unsupported("Not managed"); | ||||||||||||
| Mockito.doReturn(catalogWrapper).when(catalogManager).loadCatalogAndWrap(ident); | ||||||||||||
| Mockito.doReturn(catalog).when(catalogWrapper).catalog(); | ||||||||||||
| Mockito.doReturn(capability).when(catalogWrapper).capabilities(); | ||||||||||||
| Mockito.doReturn(unsupportedResult).when(capability).managedStorage(any()); | ||||||||||||
|
|
||||||||||||
| boolean dropped = catalogManager.dropCatalog(ident); | ||||||||||||
| Assertions.assertTrue(dropped); | ||||||||||||
|
|
||||||||||||
| // Test drop non-existed catalog | ||||||||||||
| NameIdentifier ident1 = NameIdentifier.of("metalake", "test42"); | ||||||||||||
| boolean dropped1 = catalogManager.dropCatalog(ident1); | ||||||||||||
| Assertions.assertFalse(dropped1); | ||||||||||||
|
|
||||||||||||
| // Drop operation will update the cache | ||||||||||||
| catalogManager.createCatalog(ident, Catalog.Type.RELATIONAL, provider, "comment", props); | ||||||||||||
| CatalogEntity originalEntity = entityStore.get(ident, EntityType.CATALOG, CatalogEntity.class); | ||||||||||||
| FieldUtils.writeField(catalog, "entity", originalEntity, true); | ||||||||||||
|
|
||||||||||||
| CatalogManager.CatalogWrapper staleWrapper = | ||||||||||||
| Mockito.mock(CatalogManager.CatalogWrapper.class, Mockito.RETURNS_DEEP_STUBS); | ||||||||||||
| Mockito.doReturn(catalog).when(staleWrapper).catalog(); | ||||||||||||
|
|
||||||||||||
| CatalogManager.CatalogWrapper freshWrapper = | ||||||||||||
| Mockito.mock(CatalogManager.CatalogWrapper.class, Mockito.RETURNS_DEEP_STUBS); | ||||||||||||
| Catalog freshCatalog = Mockito.mock(Catalog.class); | ||||||||||||
| CatalogEntity freshEntity = | ||||||||||||
| CatalogEntity.builder() | ||||||||||||
| .withId(originalEntity.id()) | ||||||||||||
| .withName("cache_race_test_renamed") | ||||||||||||
| .withNamespace(originalEntity.namespace()) | ||||||||||||
| .withType(originalEntity.getType()) | ||||||||||||
| .withProvider(originalEntity.getProvider()) | ||||||||||||
| .withComment(originalEntity.getComment()) | ||||||||||||
| .withProperties(originalEntity.getProperties()) | ||||||||||||
| .withAuditInfo(originalEntity.auditInfo()) | ||||||||||||
| .build(); | ||||||||||||
| FieldUtils.writeField(freshCatalog, "entity", freshEntity, true); | ||||||||||||
| Mockito.doReturn(freshCatalog).when(freshWrapper).catalog(); | ||||||||||||
|
|
||||||||||||
| AtomicBoolean staleInserted = new AtomicBoolean(false); | ||||||||||||
| Answer<CatalogManager.CatalogWrapper> insertStaleWrapper = | ||||||||||||
| invocation -> { | ||||||||||||
| if (staleInserted.compareAndSet(false, true)) { | ||||||||||||
| catalogManager.getCatalogCache().put(NameIdentifier.of("metalake", "cache_race_test_renamed"), staleWrapper); | ||||||||||||
|
||||||||||||
| catalogManager.getCatalogCache().put(NameIdentifier.of("metalake", "cache_race_test_renamed"), staleWrapper); | |
| catalogManager | |
| .getCatalogCache() | |
| .put( | |
| NameIdentifier.of("metalake", "cache_race_test_renamed"), staleWrapper); |
Copilot
AI
Apr 10, 2026
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.
This Answer returns null, which would cause an NPE if the stub were actually used as the return value of createCatalogWrapper(...). Ensure the stub returns a valid CatalogWrapper (and trigger the stale-cache insertion as a side effect if needed).
Copilot
AI
Apr 10, 2026
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.
This test stubs CatalogManager#createCatalogWrapper(...), but the method is private in CatalogManager, so this code will not compile (and cannot be stubbed with plain Mockito). Introduce a non-private seam for wrapper creation (e.g., injectable factory) or adjust the test to stub public/protected methods instead.
Copilot
AI
Apr 10, 2026
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.
The doReturn(freshWrapper) stubbing here uses the same matchers as the preceding doAnswer(...), which overrides the earlier stub. As a result, the intended race simulation never executes. Use sequential stubbing (doAnswer(...).doReturn(...)) or consolidate into a single Answer.
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.
If
backend.delete(...)throwsNoSuchEntityException, this method returnsfalsewithout invalidating the cache entry. That can leave a stale cached entity even though the backend reports it missing. Invalidate the cache in the exception path as well (or use afinally).