-
Notifications
You must be signed in to change notification settings - Fork 772
[#10545] improvement(auth): Batch-load securable objects to eliminate N+1 in loadRolePrivilege #10546
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?
[#10545] improvement(auth): Batch-load securable objects to eliminate N+1 in loadRolePrivilege #10546
Changes from all commits
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,14 +27,16 @@ | |||||||||||||||||||||||
| import java.security.Principal; | ||||||||||||||||||||||||
| import java.util.ArrayList; | ||||||||||||||||||||||||
| import java.util.Arrays; | ||||||||||||||||||||||||
| import java.util.Collections; | ||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||
| import java.util.Objects; | ||||||||||||||||||||||||
| import java.util.Optional; | ||||||||||||||||||||||||
| import java.util.concurrent.CompletableFuture; | ||||||||||||||||||||||||
| import java.util.concurrent.Executor; | ||||||||||||||||||||||||
| import java.util.concurrent.Executors; | ||||||||||||||||||||||||
| import java.util.concurrent.ThreadPoolExecutor; | ||||||||||||||||||||||||
| import java.util.concurrent.TimeUnit; | ||||||||||||||||||||||||
| import java.util.stream.Collectors; | ||||||||||||||||||||||||
| import org.apache.commons.io.IOUtils; | ||||||||||||||||||||||||
| import org.apache.commons.lang3.StringUtils; | ||||||||||||||||||||||||
| import org.apache.gravitino.Configs; | ||||||||||||||||||||||||
|
|
@@ -55,6 +57,7 @@ | |||||||||||||||||||||||
| import org.apache.gravitino.meta.RoleEntity; | ||||||||||||||||||||||||
| import org.apache.gravitino.meta.UserEntity; | ||||||||||||||||||||||||
| import org.apache.gravitino.server.authorization.MetadataIdConverter; | ||||||||||||||||||||||||
| import org.apache.gravitino.storage.relational.service.RoleMetaService; | ||||||||||||||||||||||||
| import org.apache.gravitino.utils.MetadataObjectUtil; | ||||||||||||||||||||||||
| import org.apache.gravitino.utils.NameIdentifierUtil; | ||||||||||||||||||||||||
| import org.apache.gravitino.utils.PrincipalUtils; | ||||||||||||||||||||||||
|
|
@@ -490,48 +493,56 @@ private void loadRolePrivilege( | |||||||||||||||||||||||
| () -> { | ||||||||||||||||||||||||
| EntityStore entityStore = GravitinoEnv.getInstance().entityStore(); | ||||||||||||||||||||||||
| NameIdentifier userNameIdentifier = NameIdentifierUtil.ofUser(metalake, username); | ||||||||||||||||||||||||
| List<RoleEntity> entities; | ||||||||||||||||||||||||
| List<RoleEntity> roleStubs; | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| entities = | ||||||||||||||||||||||||
| roleStubs = | ||||||||||||||||||||||||
| entityStore | ||||||||||||||||||||||||
| .relationOperations() | ||||||||||||||||||||||||
| .listEntitiesByRelation( | ||||||||||||||||||||||||
| SupportsRelationOperations.Type.ROLE_USER_REL, | ||||||||||||||||||||||||
| userNameIdentifier, | ||||||||||||||||||||||||
| Entity.EntityType.USER); | ||||||||||||||||||||||||
| List<CompletableFuture<Void>> loadRoleFutures = new ArrayList<>(); | ||||||||||||||||||||||||
| for (RoleEntity role : entities) { | ||||||||||||||||||||||||
| Long roleId = role.id(); | ||||||||||||||||||||||||
| allowEnforcer.addRoleForUser(String.valueOf(userId), String.valueOf(roleId)); | ||||||||||||||||||||||||
| denyEnforcer.addRoleForUser(String.valueOf(userId), String.valueOf(roleId)); | ||||||||||||||||||||||||
| if (loadedRoles.getIfPresent(roleId) != null) { | ||||||||||||||||||||||||
| continue; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| CompletableFuture<Void> loadRoleFuture = | ||||||||||||||||||||||||
| CompletableFuture.supplyAsync( | ||||||||||||||||||||||||
| () -> { | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| return entityStore.get( | ||||||||||||||||||||||||
| NameIdentifierUtil.ofRole(metalake, role.name()), | ||||||||||||||||||||||||
| Entity.EntityType.ROLE, | ||||||||||||||||||||||||
| RoleEntity.class); | ||||||||||||||||||||||||
| } catch (Exception e) { | ||||||||||||||||||||||||
| throw new RuntimeException("Failed to load role: " + role.name(), e); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| executor) | ||||||||||||||||||||||||
| .thenAcceptAsync( | ||||||||||||||||||||||||
| roleEntity -> { | ||||||||||||||||||||||||
| loadPolicyByRoleEntity(roleEntity); | ||||||||||||||||||||||||
| loadedRoles.put(roleId, true); | ||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||
| executor); | ||||||||||||||||||||||||
| loadRoleFutures.add(loadRoleFuture); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| CompletableFuture.allOf(loadRoleFutures.toArray(new CompletableFuture[0])).join(); | ||||||||||||||||||||||||
| } catch (IOException e) { | ||||||||||||||||||||||||
| throw new RuntimeException(e); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Register user-role associations in enforcers for all roles. | ||||||||||||||||||||||||
| for (RoleEntity role : roleStubs) { | ||||||||||||||||||||||||
| allowEnforcer.addRoleForUser(String.valueOf(userId), String.valueOf(role.id())); | ||||||||||||||||||||||||
| denyEnforcer.addRoleForUser(String.valueOf(userId), String.valueOf(role.id())); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Collect stubs for roles whose policies have not yet been loaded into the enforcer. | ||||||||||||||||||||||||
| List<RoleEntity> unloadedRoleStubs = | ||||||||||||||||||||||||
| roleStubs.stream() | ||||||||||||||||||||||||
| .filter(role -> loadedRoles.getIfPresent(role.id()) == null) | ||||||||||||||||||||||||
| .collect(Collectors.toList()); | ||||||||||||||||||||||||
| if (unloadedRoleStubs.isEmpty()) { | ||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| // Batch-fetch securable objects for all unloaded roles in a single query, | ||||||||||||||||||||||||
| // eliminating the N+1 pattern of per-role entityStore.get() calls. | ||||||||||||||||||||||||
| List<Long> unloadedRoleIds = | ||||||||||||||||||||||||
| unloadedRoleStubs.stream().map(RoleEntity::id).collect(Collectors.toList()); | ||||||||||||||||||||||||
| Map<Long, List<SecurableObject>> secObjsByRoleId = | ||||||||||||||||||||||||
| RoleMetaService.batchListSecurableObjectsForRoles(unloadedRoleIds); | ||||||||||||||||||||||||
|
Comment on lines
+528
to
+529
|
||||||||||||||||||||||||
| Map<Long, List<SecurableObject>> secObjsByRoleId = | |
| RoleMetaService.batchListSecurableObjectsForRoles(unloadedRoleIds); | |
| Map<Long, List<SecurableObject>> secObjsByRoleId; | |
| try { | |
| secObjsByRoleId = RoleMetaService.batchListSecurableObjectsForRoles(unloadedRoleIds); | |
| } catch (RuntimeException e) { | |
| throw new RuntimeException( | |
| "Failed to batch-load securable objects for roles " + unloadedRoleIds | |
| + " of userId " + userId, | |
| e); | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,7 +38,9 @@ | |
| import java.lang.reflect.Field; | ||
| import java.security.Principal; | ||
| import java.util.ArrayList; | ||
| import java.util.HashMap; | ||
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.Optional; | ||
| import java.util.concurrent.Executor; | ||
| import java.util.stream.Collectors; | ||
|
|
@@ -64,6 +66,7 @@ | |
| import org.apache.gravitino.server.authorization.MetadataIdConverter; | ||
| import org.apache.gravitino.storage.relational.po.SecurableObjectPO; | ||
| import org.apache.gravitino.storage.relational.service.OwnerMetaService; | ||
| import org.apache.gravitino.storage.relational.service.RoleMetaService; | ||
| import org.apache.gravitino.storage.relational.utils.POConverters; | ||
| import org.apache.gravitino.utils.NameIdentifierUtil; | ||
| import org.apache.gravitino.utils.NamespaceUtil; | ||
|
|
@@ -98,6 +101,12 @@ public class TestJcasbinAuthorizer { | |
| private static SupportsRelationOperations supportsRelationOperations = | ||
| mock(SupportsRelationOperations.class); | ||
|
|
||
| /** | ||
| * Shared map from role ID to securable objects, consulted by the {@link RoleMetaService} mock. | ||
| * Test methods must populate this map before exercising code paths that load new roles. | ||
| */ | ||
| private static final Map<Long, List<SecurableObject>> roleSecurableObjectsMap = new HashMap<>(); | ||
|
|
||
| private static MockedStatic<PrincipalUtils> principalUtilsMockedStatic; | ||
|
|
||
| private static MockedStatic<GravitinoEnv> gravitinoEnvMockedStatic; | ||
|
|
@@ -106,12 +115,33 @@ public class TestJcasbinAuthorizer { | |
|
|
||
| private static MockedStatic<OwnerMetaService> ownerMetaServiceMockedStatic; | ||
|
|
||
| private static MockedStatic<RoleMetaService> roleMetaServiceMockedStatic; | ||
|
|
||
| private static JcasbinAuthorizer jcasbinAuthorizer; | ||
|
|
||
| private static ObjectMapper objectMapper = new ObjectMapper(); | ||
|
|
||
| @BeforeAll | ||
| public static void setup() throws IOException { | ||
| // Pre-populate known constant roles so tests don't need to set them up individually. | ||
| roleSecurableObjectsMap.put(ALLOW_ROLE_ID, ImmutableList.of(getAllowSecurableObject())); | ||
| roleSecurableObjectsMap.put(DENY_ROLE_ID, ImmutableList.of(getDenySecurableObject())); | ||
|
|
||
| // Mock RoleMetaService.batchListSecurableObjectsForRoles to avoid real DB access. | ||
| roleMetaServiceMockedStatic = mockStatic(RoleMetaService.class); | ||
| roleMetaServiceMockedStatic | ||
| .when(() -> RoleMetaService.batchListSecurableObjectsForRoles(any())) | ||
| .thenAnswer( | ||
| inv -> { | ||
| List<Long> ids = inv.getArgument(0); | ||
| com.google.common.collect.ImmutableMap.Builder<Long, List<SecurableObject>> result = | ||
| com.google.common.collect.ImmutableMap.builder(); | ||
| for (Long id : ids) { | ||
|
Comment on lines
+137
to
+139
|
||
| result.put(id, roleSecurableObjectsMap.getOrDefault(id, ImmutableList.of())); | ||
| } | ||
| return result.build(); | ||
| }); | ||
|
|
||
| OwnerMetaService ownerMetaService = mock(OwnerMetaService.class); | ||
| ownerMetaServiceMockedStatic = mockStatic(OwnerMetaService.class); | ||
| ownerMetaServiceMockedStatic.when(OwnerMetaService::getInstance).thenReturn(ownerMetaService); | ||
|
|
@@ -164,6 +194,9 @@ public static void stop() { | |
| if (gravitinoEnvMockedStatic != null) { | ||
| gravitinoEnvMockedStatic.close(); | ||
| } | ||
| if (roleMetaServiceMockedStatic != null) { | ||
| roleMetaServiceMockedStatic.close(); | ||
| } | ||
| } | ||
|
|
||
| @Test | ||
|
|
@@ -476,11 +509,7 @@ public void testHasMetadataPrivilegePermission() throws Exception { | |
| ImmutableList.of( | ||
| buildManageGrantsSecurableObject( | ||
| metalakeGrantRoleId, MetadataObject.Type.METALAKE, METALAKE))); | ||
| when(entityStore.get( | ||
| eq(NameIdentifierUtil.ofRole(METALAKE, metalakeGrantRole.name())), | ||
| eq(Entity.EntityType.ROLE), | ||
| eq(RoleEntity.class))) | ||
| .thenReturn(metalakeGrantRole); | ||
| roleSecurableObjectsMap.put(metalakeGrantRoleId, metalakeGrantRole.securableObjects()); | ||
| when(supportsRelationOperations.listEntitiesByRelation( | ||
| eq(SupportsRelationOperations.Type.ROLE_USER_REL), | ||
| eq(userNameIdentifier), | ||
|
|
@@ -503,11 +532,7 @@ public void testHasMetadataPrivilegePermission() throws Exception { | |
| ImmutableList.of( | ||
| buildManageGrantsSecurableObject( | ||
| catalogGrantRoleId, MetadataObject.Type.CATALOG, "testCatalog"))); | ||
| when(entityStore.get( | ||
| eq(NameIdentifierUtil.ofRole(METALAKE, catalogGrantRole.name())), | ||
| eq(Entity.EntityType.ROLE), | ||
| eq(RoleEntity.class))) | ||
| .thenReturn(catalogGrantRole); | ||
| roleSecurableObjectsMap.put(catalogGrantRoleId, catalogGrantRole.securableObjects()); | ||
| when(supportsRelationOperations.listEntitiesByRelation( | ||
| eq(SupportsRelationOperations.Type.ROLE_USER_REL), | ||
| eq(userNameIdentifier), | ||
|
|
@@ -536,11 +561,7 @@ public void testHasMetadataPrivilegePermission() throws Exception { | |
| tableGrantRoleId, | ||
| MetadataObject.Type.TABLE, | ||
| "testCatalog.testSchema.testTable"))); | ||
| when(entityStore.get( | ||
| eq(NameIdentifierUtil.ofRole(METALAKE, tableGrantRole.name())), | ||
| eq(Entity.EntityType.ROLE), | ||
| eq(RoleEntity.class))) | ||
| .thenReturn(tableGrantRole); | ||
| roleSecurableObjectsMap.put(tableGrantRoleId, tableGrantRole.securableObjects()); | ||
| when(supportsRelationOperations.listEntitiesByRelation( | ||
| eq(SupportsRelationOperations.Type.ROLE_USER_REL), | ||
| eq(userNameIdentifier), | ||
|
|
||
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.
New behavior (
batchListSecurableObjectsForRoles) + new mapper/provider path (listSecurableObjectsByRoleIds) is not covered by any unit/integration test in this repo (no tests reference the new method). Per project guidelines, please add coverage inTestRoleMetaServiceto validate correctness (multiple role IDs, missing roles returning empty lists) and that it works across backends (H2/PostgreSQL).