feat(gms): add entity graph cache for domain, containers, group membership and glossary#17886
feat(gms): add entity graph cache for domain, containers, group membership and glossary#17886david-leifker wants to merge 1 commit into
Conversation
❌ 1 Tests Failed:
View the top 1 failed test(s) by shortest run time
View the full list of 1 ❄️ flaky test(s)
To view more test analytics, go to the Test Analytics Dashboard |
9605333 to
1ccc9a3
Compare
1ccc9a3 to
9b7a500
Compare
9b7a500 to
7c7ce45
Compare
7c7ce45 to
5c8c894
Compare
5c8c894 to
0b56cd6
Compare
0b56cd6 to
3440567
Compare
3440567 to
b36425a
Compare
b36425a to
acb53f4
Compare
acb53f4 to
bf48dab
Compare
bf48dab to
92b19ab
Compare
Introduce a Hazelcast-backed entity graph cache with bundled domain, glossary, container, and membership graphs. Route hot GraphQL, VBAC, and search paths through cache-first client bindings with aspect and scroll fallbacks. Retain corp vs native group distinction in SessionActorIdentity so session-user membership fast paths label IsMemberOfGroup and IsMemberOfNativeGroup correctly. Co-authored-by: Cursor <cursoragent@cursor.com>
92b19ab to
ebd64b1
Compare
| private EntityGraphCacheClients() {} | ||
|
|
||
| @Nonnull | ||
| public static GraphReadResult expand( |
There was a problem hiding this comment.
Builder pattern with defaults for the method arg (i.e. ExpandRequest { default: USE_DEFINITION_MAX_DEPTH... } ? Having 4 different method entry points is going to make adding any additional params to this a nightmare.
| @Nonnull | ||
| private static Optional<KnownEntityGraph> knownGraphForPolicyField( | ||
| @Nonnull String policyFieldType) { | ||
| if ("DOMAIN".equals(policyFieldType)) { |
There was a problem hiding this comment.
nit: prefer constants
| @Nonnull MembershipReadSpec spec) { | ||
| String entityType = UrnUtils.getUrn(seedUrn).getEntityType(); | ||
| return switch (entityType) { | ||
| case "corpuser" -> new ScrollConfig( |
There was a problem hiding this comment.
nit: should use entity name constants
|
|
||
| @Nonnull @Builder.Default Set<String> roleRelationshipTypes = Set.of("IsMemberOfRole"); | ||
|
|
||
| @Nonnull @Builder.Default Set<String> scrollUserEntityTypes = Set.of("corpuser"); |
| EntityGraphSnapshot.builder() | ||
| .graphId(snapshot.getGraphId()) | ||
| .cacheKey(key) | ||
| .generation((existing == null ? 0L : existing.getGeneration()) + 1L) |
There was a problem hiding this comment.
Should there be a version gate for the generation similar to conditional writes? I think a race condition exists for surgical removes where you can wind up with an inconsistent cache.
node1: getSnapshot(a,b,c) node2: getSnapshot(a,b,c)
node1: surgicalRemove(a) node2: surgicalRemove(b)
node1: publish(b,c)->gen1
node2: publish(a,c)->gen2
| } | ||
|
|
||
| if (urnsWithKeyAspect.contains(urn)) { | ||
| String createKey = urn + "|" + aspectName; |
There was a problem hiding this comment.
Probably better to use a true separator character that can't exist in an urn, but I think this is mostly safe for the current entities it's used for since they get generated as uuid format.
|
|
||
| @Nonnull | ||
| private DirectedMultigraph<String, DirectedEdge> forwardGraph() { | ||
| if (forwardGraph == null) { |
There was a problem hiding this comment.
Not a huge deal, but this is an avoidable race condition through synchronization on building the graph which is a non-trivial amount of work since there are a lot of entry points into this.
RyanHolstien
left a comment
There was a problem hiding this comment.
There are some nitpicky things and one legitimate question around cache inconsistency, but overall approving.
Summary
Introduces a GMS entity graph cache — Hazelcast-backed, pre-built relationship snapshots that avoid repeated primary-storage aspect walks and graph/search scrolls on hot paths (VBAC policy expansion, search filter rewriters, GraphQL hierarchy/membership queries, and session authorization).
Four bundled graphs ship in
entity-graph-cache.yaml:domain@searchIsPartOftree for VBAC, search rewriters, GraphQLglossary@graphcontainer@graphmembership@graphCache is enabled by default on GMS (
ENTITY_GRAPH_CACHE_ENABLED); MAE/MCE consumers anddatahub-upgraderegisterEntityGraphCache.NO_OP.Architecture
EntityGraphCacheinterface +EntityGraphCacheService(expand, listRelated, ancestor walks, invalidation, rebuild scheduling)primary,graph, orsearchscroll depending on graph configFULL(whole graph) andPARTIAL(per weakly connected component, directional BFS capped bymaxDepth)BoundHierarchyAccess+HierarchyBindings/HierarchyReadSpecs— cache-first ancestor expand, ordered parents, direct children, descendant checksBoundMembershipAccess+MembershipBindings/MembershipReadSpecs— cache-first group/role neighbor listing with aspect and graph-scroll fallbackspreprocessEvent); async/non-gated ingest relies on scheduled/LAZY rebuild staleness windowsGraphRetrieverscroll;deleteDomainchild guard uses primary-store verify viaAspectDirectChildrenWalker(not cache-backed)Integrations
DomainFieldResolverProvider,ContainerFieldResolverProvider, andGlossaryFieldResolverProvideruse cache-first expand on bundled graphsDomainExpansionRewriterfordomains.keywordfilter expansionParentDomainsResolver,ParentNodesResolver,ParentContainersResolver, andEntityRelationshipsResultResolver(direct child domains/glossary nodes/containers) routed throughBoundHierarchyAccess;MoveDomainResolver/UpdateParentNodeResolvertrigger sync invalidationEntityRelationshipsResultResolverroutes corp user / corp group / role membership throughBoundMembershipAccess; session-user outgoing membership uses a fast path backed bySessionActorIdentitySessionActorIdentityretains separatecorpGroupsandnativeGroupssets so the session fast path labelsIsMemberOfGroupvsIsMemberOfNativeGroupcorrectly (dual-type queries no longer mislabel native memberships)EntityGraphCacheexposed viaRetrieverContext;ActorGroupMembershipService/GroupServicepopulate typed group membershipCacheConfigConfiguration & docs
metadata-service/configuration/src/main/resources/entity-graph-cache.yamlEntityGraphCacheProperties,EntityGraphCacheConfigLoaderdocs/deploy/gms-entity-graph-cache.mddocs/deploy/environment-vars.mdTest plan
./gradlew spotlessCheck./gradlew :li-utils:test --tests com.datahub.authorization.SessionActorIdentityTest./gradlew :datahub-graphql-core:test --tests com.linkedin.datahub.graphql.resolvers.load.EntityRelationshipsResultResolverTestmetadata-ioentity graph cache unit + Hazelcast integration testsEntityGraphCacheConfigLoaderTest, factory map config testssmoke-test/tests/entity_graph_cache/test_entity_graph_cache.py(domain, glossary, container hierarchy + session-user membership idempotency)types: [IsMemberOfGroup, IsMemberOfNativeGroup]— verify relationship type per edge matches cache/scroll path