Skip to content

Commit 3fea897

Browse files
authored
Fix NPE in listCatalogs (#1949)
listCatalogs is non-atomic. It first atomically lists all entities and then iterates through each one and does an individual loadEntity call. This causes an NPE when calling `CatalogEntity::new`. I don't think it's ever useful for listCatalogsUnsafe to return null since the caller isn't expecting a certain length of elements, so I just filtered it there.
1 parent e7a009f commit 3fea897

File tree

2 files changed

+83
-5
lines changed

2 files changed

+83
-5
lines changed

runtime/service/src/test/java/org/apache/polaris/service/quarkus/admin/ManagementServiceTest.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,13 @@
2121
import static org.assertj.core.api.Assertions.assertThat;
2222
import static org.assertj.core.api.Assertions.assertThatThrownBy;
2323

24+
import jakarta.annotation.Nonnull;
2425
import jakarta.ws.rs.core.Response;
2526
import jakarta.ws.rs.core.SecurityContext;
2627
import java.security.Principal;
2728
import java.time.Clock;
2829
import java.time.Instant;
30+
import java.util.HashSet;
2931
import java.util.List;
3032
import java.util.Map;
3133
import java.util.Set;
@@ -43,11 +45,19 @@
4345
import org.apache.polaris.core.auth.PolarisAuthorizerImpl;
4446
import org.apache.polaris.core.context.CallContext;
4547
import org.apache.polaris.core.context.RealmContext;
48+
import org.apache.polaris.core.entity.PolarisBaseEntity;
49+
import org.apache.polaris.core.entity.PolarisEntity;
50+
import org.apache.polaris.core.entity.PolarisEntityConstants;
51+
import org.apache.polaris.core.entity.PolarisEntitySubType;
52+
import org.apache.polaris.core.entity.PolarisEntityType;
4653
import org.apache.polaris.core.entity.PrincipalEntity;
4754
import org.apache.polaris.core.entity.PrincipalRoleEntity;
4855
import org.apache.polaris.core.persistence.MetaStoreManagerFactory;
4956
import org.apache.polaris.core.persistence.PolarisMetaStoreManager;
57+
import org.apache.polaris.core.persistence.dao.entity.BaseResult;
58+
import org.apache.polaris.core.persistence.dao.entity.CreateCatalogResult;
5059
import org.apache.polaris.core.persistence.dao.entity.EntityResult;
60+
import org.apache.polaris.core.persistence.transactional.TransactionalMetaStoreManagerImpl;
5161
import org.apache.polaris.core.secrets.UnsafeInMemorySecretsManager;
5262
import org.apache.polaris.service.TestServices;
5363
import org.apache.polaris.service.admin.PolarisAdminService;
@@ -276,4 +286,66 @@ public void testCannotAssignFederatedEntities() {
276286
() -> polarisAdminService.assignPrincipalRole(principal.getName(), role.getName()))
277287
.isInstanceOf(ValidationException.class);
278288
}
289+
290+
/** Simulates the case when a catalog is dropped after being found while listing all catalogs. */
291+
@Test
292+
public void testCatalogNotReturnedWhenDeletedAfterListBeforeGet() {
293+
TestPolarisMetaStoreManager metaStoreManager = new TestPolarisMetaStoreManager();
294+
PolarisCallContext callContext = setupCallContext(metaStoreManager);
295+
PolarisAdminService polarisAdminService =
296+
setupPolarisAdminService(metaStoreManager, callContext);
297+
298+
CreateCatalogResult catalog1 =
299+
metaStoreManager.createCatalog(
300+
callContext,
301+
new PolarisBaseEntity(
302+
PolarisEntityConstants.getNullId(),
303+
metaStoreManager.generateNewEntityId(callContext).getId(),
304+
PolarisEntityType.CATALOG,
305+
PolarisEntitySubType.NULL_SUBTYPE,
306+
PolarisEntityConstants.getRootEntityId(),
307+
"my-catalog-1"),
308+
List.of());
309+
CreateCatalogResult catalog2 =
310+
metaStoreManager.createCatalog(
311+
callContext,
312+
new PolarisBaseEntity(
313+
PolarisEntityConstants.getNullId(),
314+
metaStoreManager.generateNewEntityId(callContext).getId(),
315+
PolarisEntityType.CATALOG,
316+
PolarisEntitySubType.NULL_SUBTYPE,
317+
PolarisEntityConstants.getRootEntityId(),
318+
"my-catalog-2"),
319+
List.of());
320+
321+
metaStoreManager.setFakeEntityNotFoundIds(Set.of(catalog1.getCatalog().getId()));
322+
List<PolarisEntity> catalogs = polarisAdminService.listCatalogs();
323+
assertThat(catalogs.size()).isEqualTo(1);
324+
assertThat(catalogs.getFirst().getId()).isEqualTo(catalog2.getCatalog().getId());
325+
}
326+
327+
/**
328+
* Intended to be a delegate to TransactionalMetaStoreManagerImpl with the ability to inject
329+
* faults. Currently, you can force loadEntity() to return ENTITY_NOT_FOUND for a set of entity
330+
* IDs.
331+
*/
332+
public static class TestPolarisMetaStoreManager extends TransactionalMetaStoreManagerImpl {
333+
private Set<Long> fakeEntityNotFoundIds = new HashSet<>();
334+
335+
public void setFakeEntityNotFoundIds(Set<Long> ids) {
336+
fakeEntityNotFoundIds = new HashSet<>(ids);
337+
}
338+
339+
@Override
340+
public @Nonnull EntityResult loadEntity(
341+
@Nonnull PolarisCallContext callCtx,
342+
long entityCatalogId,
343+
long entityId,
344+
@Nonnull PolarisEntityType entityType) {
345+
if (fakeEntityNotFoundIds.contains(entityId)) {
346+
return new EntityResult(BaseResult.ReturnStatus.ENTITY_NOT_FOUND, "");
347+
}
348+
return super.loadEntity(callCtx, entityCatalogId, entityId, entityType);
349+
}
350+
}
279351
}

service/common/src/main/java/org/apache/polaris/service/admin/PolarisAdminService.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -614,7 +614,6 @@ private boolean catalogOverlapsWithExistingCatalog(CatalogEntity catalogEntity)
614614

615615
Set<String> newCatalogLocations = getCatalogLocations(catalogEntity);
616616
return listCatalogsUnsafe().stream()
617-
.filter(Objects::nonNull)
618617
.map(CatalogEntity::new)
619618
.anyMatch(
620619
existingCatalog -> {
@@ -923,18 +922,24 @@ private void validateUpdateCatalogDiffOrThrow(
923922
return returnedEntity;
924923
}
925924

925+
/**
926+
* List all catalogs after checking for permission. Nulls due to non-atomic list-then-get are
927+
* filtered out.
928+
*/
926929
public List<PolarisEntity> listCatalogs() {
927930
authorizeBasicRootOperationOrThrow(PolarisAuthorizableOperation.LIST_CATALOGS);
928931
return listCatalogsUnsafe();
929932
}
930933

931934
/**
932-
* List all catalogs without checking for permission. May contain NULLs due to multiple non-atomic
933-
* API calls to the persistence layer. Specifically, this can happen when a PolarisEntity is
934-
* returned by listCatalogs, but cannot be loaded afterward because it was purged by another
935-
* process before it could be loaded.
935+
* List all catalogs without checking for permission. Nulls due to non-atomic list-then-get are
936+
* filtered out.
936937
*/
937938
private List<PolarisEntity> listCatalogsUnsafe() {
939+
// loadEntity may return null due to multiple non-atomic
940+
// API calls to the persistence layer. Specifically, this can happen when a PolarisEntity is
941+
// returned by listCatalogs, but cannot be loaded afterward because it was purged by another
942+
// process before it could be loaded.
938943
return metaStoreManager
939944
.listEntities(
940945
getCurrentPolarisContext(),
@@ -949,6 +954,7 @@ private List<PolarisEntity> listCatalogsUnsafe() {
949954
PolarisEntity.of(
950955
metaStoreManager.loadEntity(
951956
getCurrentPolarisContext(), 0, nameAndId.getId(), nameAndId.getType())))
957+
.filter(Objects::nonNull)
952958
.toList();
953959
}
954960

0 commit comments

Comments
 (0)