Rebuilding OpenIAM's in-memory authorization graph
The Rebuild Graph operation fully resets and reconstructs OpenIAM's in-memory authorization graph. The authorization graph is the data structure the system uses to answer every entitlement question in real time: "Is this user in this group?", "Does this user have access to this resource?", etc.
Triggering a rebuild causes the Authorization Manager service to:
- Drop the current graph entirely (all vertices and edges).
- Clear all associated caches (local + Redis).
- Re-fetch all authorization data from the relational database.
- Rebuild the graph from scratch and repopulate the caches.
For end-users / System administrators
There are several situation in which the graph rebuild function should be used, for example:
- After a database migration or direct DB modification.
- As s routine operation / scheduled maintenance. Here, the graph rebuilding is optional since the graph rebuilds automatically on first startup if the graph is empty.
In normal day-to-day operations, the function is not to be used since the graph stays in sync automatically.
How to trigger?
Send an HTTP GET request to the Authorization Manager REST endpoint:
GET /authmanager/rebuildGraph
Example (curl):
curl -X GET http://<your-openiam-host>:9080/authmanager/rebuildGraph
Expected behavior
- The operation is asynchronous — the API returns immediately; the actual rebuild happens in the background.
- The rebuild can take a significant amount of time depending on the size of your data (users, groups, roles, resources, organizations).
- During the rebuild the Authorization Manager instance is unavailable for authorization checks until the process completes.
- If the rebuild fails for any reason, the service will shut itself down (to avoid serving stale/incorrect data). It will need to be restarted — on restart the service checks whether the JanusGraph is empty; if it is, it triggers a rebuild automatically in a background thread.
- Concurrent rebuild requests are safe: a Redis-distributed lock ensures only one rebuild runs at a time across all instances.
Monitoring progress
Check the Authorization Manager service logs for entries like:
[WARN] Creating graph from current data. This may take a long time...[INFO] Time to get all data from relational database: <N> ms[INFO] Done inserting <N> vertices into graph database...[INFO] Creation (or fail) of authorization objects in graph database took <N> ms
A successful completion produces the final timing log line. Any error will be logged at ERROR level.
For Developers
REST Entry Point
Controller: AuthManagerRestController
File: openiam-esb/src/main/java/org/openiam/esb/rest/AuthManagerRestController.java
GET /authmanager/rebuildGraph
Calls authManagerMQService.refreshCache() — a fire-and-forget async message.
Full call chain
GET /authmanager/rebuildGraph│▼AuthManagerRestController.rebuildGraph()│▼AuthManagerMQServiceImpl.refreshCache()│ Sends async RabbitMQ message:│ API: AMManagerAPI.RefreshAMManager│ Payload: EmptyServiceRequest│ Exchange: AM_EXCHANGE (virtual host: AM_HOST)▼RabbitMQSenderImpl.sendAndReceive()│ Routes to the correct RequestServiceGateway by vhost▼AMManagerQueueListener (RabbitMQ consumer on AMManagerQueue)│ getEmptyRequestProcessor() → case RefreshAMManager▼AuthorizationManagerServiceImpl.rebuildGraph()│├─ graphOperations.deleteAllIndicies() // drop all vertices from JanusGraph├─ remoteEntitlementsCache.delete(keys) // clear Redis remote entitlements cache└─ synchronized(localEntitlementsCacheLock)├─ sweep() // the main rebuild (see below)├─ localEntitlementsCache.invalidateAll()├─ graphIdCacheSweeper.forceSweep()├─ entitlementsObjectsCacheSweeper.forceSweep()└─ edgeIdCacheSweeper.forceSweep()
Graph rebuild detail
The sweep() method steps are given below.
File: auth-manager/src/main/java/org/openiam/authmanager/service/impl/AuthorizationManagerServiceImpl.java
This is where the actual graph is constructed. It runs under a Redis distributed lock (GRAPH_BUILDER_LOCK_NAME, 10-second acquisition timeout).
Acquire distributed lock — prevents concurrent rebuilds across multiple service instances.
Fetch relational data (see SQL Queries below):
dataProvider.getModel(NonCachedEntitlementRequest)— loads all organizations, roles, groups, resources, and their membership relationships.dataProvider.getUsers()— loads all users.
Build vertices —
buildVertex2OpeniamGraphTuple()creates JanusGraph vertices for each entity type:- Users
- Organizations
- Roles
- Groups
- Resources
Persist graph IDs to relational DB — each vertex gets a graph ID that is written back to the RDBMS in batches (
batchSize) inside a transaction (viaauthManagerDAO.updateGraphId()).Build edges —
addEdges()creates directed edges in JanusGraph representing all membership and entitlement relationships (user→group, group→role, role→resource, etc.).Refresh caches:
graphIdCacheSweeper.forceSweep()entitlementsObjectsCacheSweeper.forceSweep()edgeIdCacheSweeper.forceSweep()
On failure — all indices are dropped and
SpringApplication.exit()is called with exit code 1. The service shuts down to avoid serving incorrect data.
Error Handling
| Scenario | Behaviour |
|---|---|
| Lock not acquired within 10 s | sweep() silently skips; error logged |
Any Throwable during build | All graph indices dropped; service exits with code 1 |
| Successful completion | All caches refreshed; graph is live |
Startup behavior
Does it always rebuild?
The graph is only rebuilt on startup if JanusGraph contains no vertices.
This means:
- First deploy / fresh environment — graph is empty → full rebuild runs automatically.
- Normal restart — JanusGraph already has data → rebuild is skipped; existing graph is used immediately.
- After a crash that called
SpringApplication.exit()— the crash first drops all indices, so on the next restart the graph is empty and rebuilds automatically.
This also implies that if JanusGraph data is manually cleared outside of the application, the next service restart will trigger a full rebuild.
Service: auth-manager (AuthorizationManagerServiceImpl)
File: auth-manager/src/main/java/org/openiam/authmanager/service/impl/AuthorizationManagerServiceImpl.java
On startup, @PostConstruct init() runs the following check:
if (isEmptyGraph()) {Executors.newSingleThreadExecutor().submit(() -> {sweep(); // full graph rebuild in a background thread});} else {log.info("Graph not empty - not populating. Found at least one vertex");}
SQL queries used during rebuild
All queries are issued by JdbcMembershipDAO and JDBCAccessRightDAO (both in openiam-common-boot-module). The {schema} prefix is a configurable table-name prefix (empty by default).
Entity queries
-- UsersSELECT USER_ID AS ID, GRAPH_ID FROM {schema}USERS-- ResourcesSELECT GRAPH_ID, RESOURCE_ID AS ID, NAME, DESCRIPTION, RESOURCE_TYPE_ID,RISK, COORELATED_NAME, IS_PUBLIC, TYPE_IDFROM {schema}RES-- GroupsSELECT GRAPH_ID, GRP_ID AS ID, GRP_NAME AS NAME, GROUP_DESC AS DESCRIPTION,STATUS, MANAGED_SYS_ID, TYPE_IDFROM {schema}GRP-- RolesSELECT GRAPH_ID, ROLE_ID AS ID, ROLE_NAME AS NAME, DESCRIPTION,STATUS, MANAGED_SYS_ID, TYPE_IDFROM {schema}ROLE-- OrganizationsSELECT GRAPH_ID, COMPANY_ID AS ID, COMPANY_NAME AS NAME, DESCRIPTION, STATUSFROM {schema}COMPANY-- Access RightsSELECT ACCESS_RIGHT_ID AS ID, NAME FROM {schema}ACCESS_RIGHTS
Membership (Edge) query
All membership relationships are loaded in a single query — a UNION ALL of 15 cross-reference tables wrapped in a subquery:
SELECT MEMBER_ENTITY_ID, ENTITY_ID, MEMBERSHIP_ID, TYPE, START_DATE, END_DATE, EDGE_IDFROM (SELECT ... FROM {schema}USER_ROLE -- user → roleUNION ALLSELECT ... FROM {schema}USER_GRP -- user → groupUNION ALLSELECT ... FROM {schema}USER_AFFILIATION -- user → organizationUNION ALLSELECT ... FROM {schema}RESOURCE_USER -- user → resourceUNION ALLSELECT ... FROM {schema}COMPANY_TO_COMPANY_MEMBERSHIP -- org → orgUNION ALLSELECT ... FROM {schema}ROLE_ORG_MEMBERSHIP -- org → roleUNION ALLSELECT ... FROM {schema}GROUP_ORGANIZATION -- org → groupUNION ALLSELECT ... FROM {schema}RES_ORG_MEMBERSHIP -- org → resourceUNION ALLSELECT ... FROM {schema}role_to_role_membership -- role → roleUNION ALLSELECT ... FROM {schema}GRP_ROLE -- role → groupUNION ALLSELECT ... FROM {schema}RESOURCE_ROLE -- role → resourceUNION ALLSELECT ... FROM {schema}grp_to_grp_membership -- group → groupUNION ALLSELECT ... FROM {schema}RESOURCE_GROUP -- group → resourceUNION ALLSELECT ... FROM {schema}res_to_res_membership -- resource → resourceUNION ALLSELECT ... FROM {schema}ORG_STRUCTURE -- user → user (hierarchy)) OPTIMIZED_SUBQUERY
When a date parameter is provided (e.g., when only fetching recently changed memberships), each sub-select adds a date-range filter on START_DATE / END_DATE.
Membership rights query
Access rights attached to each membership edge are loaded separately — another UNION ALL across 14 rights tables:
SELECT MEMBERSHIP_ID, ACCESS_RIGHT_ID, '{Type}' AS TYPEFROM {schema}USER_ROLE_MEMBERSHIP_RIGHTSUNION ALLSELECT MEMBERSHIP_ID, ACCESS_RIGHT_ID, '{Type}' AS TYPEFROM {schema}USER_GRP_MEMBERSHIP_RIGHTSUNION ALL-- ... (USER_AFFILIATION_RIGHTS, USER_RES_MEMBERSHIP_RIGHTS,-- ORG_TO_ORG_MEMBERSHIP_RIGHTS, ROLE_ORG_MEMBERSHIP_RIGHTS,-- GRP_ORG_MEMBERSHIP_RIGHTS, RES_ORG_MEMBERSHIP_RIGHTS,-- ROLE_ROLE_MEMBERSHIP_RIGHTS, GRP_ROLE_MEMBERSHIP_RIGHTS,-- RES_ROLE_MEMBERSHIP_RIGHTS, GRP_GRP_MEMBERSHIP_RIGHTS,-- RES_GRP_MEMBERSHIP_RIGHTS, RES_RES_MEMBERSHIP_RIGHTS)
Summary of tables read
| Table | Content |
|---|---|
USERS | All user accounts. |
RES | All resources. |
GRP | All groups. |
ROLE | All roles. |
COMPANY | All organizations. |
ACCESS_RIGHTS | All access right definitions. |
USER_ROLE, USER_GRP, USER_AFFILIATION, RESOURCE_USER | User memberships. |
COMPANY_TO_COMPANY_MEMBERSHIP, ROLE_ORG_MEMBERSHIP, GROUP_ORGANIZATION, RES_ORG_MEMBERSHIP | Organization memberships. |
role_to_role_membership, GRP_ROLE, RESOURCE_ROLE | Role memberships. |
grp_to_grp_membership, RESOURCE_GROUP | Group memberships. |
res_to_res_membership | Resource hierarchy. |
ORG_STRUCTURE | User–user hierarchy. |
*_MEMBERSHIP_RIGHTS tables (×14) | Rights attached to each membership edge. |
EDGE_ID Lifecycle
EDGE_ID is a column present on every membership xref table (e.g., USER_ROLE, GRP_ROLE, RESOURCE_USER, etc.) and on every membership rights table (e.g., USER_ROLE_MEMBERSHIP_RIGHTS). It stores the ID of the corresponding edge in JanusGraph, linking relational membership records to graph edges.
When addEdges() creates an edge in JanusGraph via Gremlin, the traversal uses .as(selectKey) to label each created edge and then .select(...) to retrieve the resulting Edge objects. The edge.id().toString() value returned by JanusGraph is the EDGE_ID that gets written back to the relational DB.
All writes go through AuthManagerDAOImpl (auth-manager module). Three SQL patterns are used:
-- Membership with an access right attachedUPDATE {schema}{xref_rights_table} SET EDGE_ID = ? WHERE MEMBERSHIP_ID = ? AND ACCESS_RIGHT_ID = ?-- Membership with no access rightUPDATE {schema}{xref_table} SET EDGE_ID = ? WHERE MEMBERSHIP_ID = ?-- Nulling out a stale/expired EDGE_IDUPDATE {schema}{xref_table} SET EDGE_ID = NULL WHERE EDGE_ID = ?UPDATE {schema}{xref_rights_table} SET EDGE_ID = NULL WHERE EDGE_ID = ?
The target table is resolved at runtime from an internal map keyed on (parentVertexType, childVertexType) — e.g., (USER, ROLE) → USER_ROLE / USER_ROLE_MEMBERSHIP_RIGHTS.
The EDGE_ID is written in the following cases.
| Event | What happens to EDGE_ID |
|---|---|
Full graph rebuild (sweep()) | All edges recreated in JanusGraph; EDGE_ID written back in batches for every membership row via authManagerDAO.updateEdges() / updateEdgesWithoutRights(). |
Single edge add/update (GraphOperations.addEdges(SaveGraphEdgeRequest)) | Old edge deleted from JanusGraph first, new edge created, new EDGE_ID written back; edge ID cache updated locally and broadcast to all nodes. |
Expired edge removal (removeExpiredEdges()) | Expired edges dropped from JanusGraph; corresponding EDGE_ID columns set to NULL via authManagerDAO.nullOutEdges(). |
Data inconsistency fix (fixDataInconsistencies()) | Queries memberships where EDGE_ID IS NULL (isOnlyIncludeMembershipsNotInsertedIntoGraphDatabase = true), creates the missing edges in JanusGraph, then writes the new EDGE_ID values back. |
fixDataInconsistencies() runs on a schedule (org.openiam.authorization.manager.gremlin.fix.data.time.ms) in addition to being triggerable via the AMManagerAPI.FixDataInconsistencies endpoint.
After writing to the DB, the EDGE_ID → ACCESS_RIGHT_ID mapping is synced into a Redis cache and local in-process cache via EdgeIdCacheSweeper. This cache is used during authorization checks to resolve edge rights without hitting the DB. The sweeper also runs on a fixed schedule (org.openiam.edge.id.threadsweep).
The complete flow for a single-edge update:
addEdges(SaveGraphEdgeRequest)│├─ deleteEdge(oldEdgeId) // remove stale edge from JanusGraph├─ createEdgeTraversal(...) // create new edge; Gremlin returns edge.id()├─ authManagerDAO.updateEdges(...) // UPDATE {table} SET EDGE_ID=? WHERE MEMBERSHIP_ID=? AND ACCESS_RIGHT_ID=?├─ authManagerDAO.updateEdgesWithoutRights(...) // UPDATE {table} SET EDGE_ID=? WHERE MEMBERSHIP_ID=?├─ edgeIdCache.refreshTemporaryCacheEntry(...) // update local in-process cache└─ authManagerAdminMQService.refreshEdgeId(...) // broadcast to other cluster nodes
GRAPH_ID Lifecycle
GRAPH_ID is a column on every entity table (USERS, GRP, ROLE, COMPANY, RES). It stores the JanusGraph vertex ID for that entity, linking each relational row to its vertex in the graph.
JPA note: The
graphIdfield on all entity classes (UserEntity,GroupEntity,RoleEntity,OrganizationEntity,ResourceEntity) is mapped withinsertable = false, updatable = false. JPA never writes this column — all writes go through direct JDBC inAuthManagerDAOImpl.
Write-back SQL looks like the following.
-- UsersUPDATE {schema}USERS SET GRAPH_ID = ? WHERE USER_ID = ?-- GroupsUPDATE {schema}GRP SET GRAPH_ID = ? WHERE GRP_ID = ?-- Roles (only for roles not excluded from auth)UPDATE {schema}ROLE SET GRAPH_ID = ? WHERE ROLE_ID = ? AND EXCLUDE_FROM_AUTH = 'N'-- OrganizationsUPDATE {schema}COMPANY SET GRAPH_ID = ? WHERE COMPANY_ID = ?-- ResourcesUPDATE {schema}RES SET GRAPH_ID = ? WHERE RESOURCE_ID = ?
The target table and primary-key column are resolved from a map keyed on VertexType (USER, GROUP, ROLE, ORGANIZATION, RESOURCE).
The GRAPH_ID is written in the following cases.
| Event | What happens to GRAPH_ID |
|---|---|
Full graph rebuild (sweep()) | All entities inserted as vertices into JanusGraph; vertex.id().toString() written back in batches via authManagerDAO.updateGraphId(type, List<Tuple>) |
Data inconsistency fix (fixDataInconsistencies()) | Same batch path — called for entities found to be missing from the graph |
New entity created at runtime (GraphOperations.addVertex()) | Checks graphIdProvider.contains(type, entityId) first; if absent, creates a single vertex and writes its ID back via authManagerDAO.updateGraphId(type, id, graphId) in a transaction |
The single-entity path (addVertex) is the normal path when a new user, group, role, org, or resource is provisioned through the application — no full rebuild is needed.
During a full rebuild buildVertex2OpeniamGraphTuple() calls addObjectsToGraph() for each entity type. addObjectsToGraph() batches entities (graphBatchSize), sends them to JanusGraph via Gremlin, reads back the Vertex objects, and returns List<Tuple<openiamId, vertex.id()>>. Those tuples are then persisted back in RDBMS batches.
After writing, GraphIdCacheSweeper.forceSweep() is called. It:
- Reads all current
GRAPH_IDvalues from every entity table into aMap<VertexType, Map<openiamId, graphId>>. - Stores the maps in Redis (one key per
VertexType). - Refreshes the local in-process Guava cache (
AbstractGraphIdProvider.sweep()). - Broadcasts
refreshGraphIdCacheto all cluster nodes via RabbitMQ so every instance updates its local copy.
The local cache has a 10-minute write TTL and also a scheduled periodic sync (org.openiam.graph.id.threadsweep). For new single entities, graphIdProvider.refreshTemporaryCacheEntry() updates the local cache immediately without waiting for the next sweep.
The complete flow for a single new entity:
GraphOperations.addVertex(type, entity, properties)│├─ graphIdProvider.contains(type, id)? → skip if already exists├─ graphSource.addV(type).property(...).next() → vertex.id() is the new GRAPH_ID├─ authManagerDAO.updateGraphId(type, entityId, graphId)│ UPDATE {schema}{table} SET GRAPH_ID=? WHERE {pk}=?├─ graphIdProvider.refreshTemporaryCacheEntry(type, id, graphId) → local Guava cache├─ entitlementsObjectCache.addCacheEntry(entity)└─ authManagerAdminMQService.refreshGraphId(entity)→ broadcasts to all nodes via RabbitMQ→ each node: AMCacheQueueListener → graphIdProvider.refreshTemporaryCacheEntry()
GRAPH_ID is treated as a String in the DB in all cases. The type conversion (e.g. Long for JanusGraph, String for Neptune / CosmosDB) is handled by the GraphIdProvider implementation at query time, not at write time.
Key classes
| Class | Module | Role |
|---|---|---|
AuthManagerRestController | openiam-esb | REST endpoint. |
AuthManagerMQServiceImpl | openiam-mq-services | Sends RabbitMQ message. |
AMManagerQueueListener | auth-manager | RabbitMQ consumer; routes to service. |
AuthorizationManagerServiceImpl | auth-manager | Orchestrates the rebuild; startup empty-graph check. |
AuthorizationManagerDataProvider | auth-manager | Fetches data model from DB. |
JdbcMembershipDAO | openiam-common-boot-module | Executes all entity + membership SQL queries. |
JDBCAccessRightDAO | openiam-common-boot-module | Fetches access right definitions. |
GraphOperations | auth-manager | Low-level JanusGraph operations. |
GraphIdCacheSweeper | auth-manager | Refreshes graph-ID cache. |
EdgeIdCacheSweeper | auth-manager | Refreshes edge-ID cache. |
AMManagerAPI | openiam-common-intf | Enum of AM API names. |
AMManagerQueue / AMQueue | openiam-common-intf | RabbitMQ queue/exchange config. |
Caches cleared during rebuild
| Cache | Type | Cleared by |
|---|---|---|
| Remote entitlements cache | Redis (prefix REMOTE_ENTITLEMENTS_CACHE_KEY_PREFIX*) | rebuildGraph() |
| Local entitlements cache | Guava/Caffeine in-process | localEntitlementsCache.invalidateAll() |
| Graph ID cache | In-process / distributed | GraphIdCacheSweeper.forceSweep() |
| Entitlements objects cache | In-process / distributed | EntitlementsObjectsCacheSweeper.forceSweep() |
| Edge ID cache | In-process / distributed | EdgeIdCacheSweeper.forceSweep() |
Graph technology
The authorization graph is stored in JanusGraph (accessed via Gremlin traversal API — graphSource.V(), .drop(), etc.). GraphOperations.deleteAllIndicies() iterates and drops vertices in batches of 100 until the graph is empty.
Thread safety
rebuildGraph()uses asynchronizedblock onlocalEntitlementsCacheLockto serialize local cache operations.sweep()uses a Redis distributed lock (redissonClient.getLock(GRAPH_BUILDER_LOCK_NAME)) to serialize the graph rebuild across all service instances in a cluster.