{"record":{"id":"2bd4b70772ae8e3e","repo":"apache/druid","slug":"could-not-unassign-role-s-from-user-s-due-to","errorCode":null,"errorMessage":"Could not unassign role [%s] from user [%s] due to concurrent update contention.","messagePattern":"Could not unassign role \\[(.+?)\\] from user \\[(.+?)\\] due to concurrent update contention\\.","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java","lineNumber":802,"sourceCode":"  }\n\n  private void unassignUserRoleInternal(String prefix, String userName, String roleName)\n  {\n    int attempts = 0;\n    while (attempts < numRetries) {\n      if (unassignUserRoleOnce(prefix, userName, roleName)) {\n        return;\n      } else {\n        attempts++;\n      }\n      try {\n        Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));\n      }\n      catch (InterruptedException ie) {\n        throw new RuntimeException(ie);\n      }\n    }\n    throw new ISE(\"Could not unassign role [%s] from user [%s] due to concurrent update contention.\", roleName, userName);\n  }\n\n  private void assignGroupMappingRoleInternal(String prefix, String groupMappingName, String roleName)\n  {\n    int attempts = 0;\n    while (attempts < numRetries) {\n      if (assignGroupMappingRoleOnce(prefix, groupMappingName, roleName)) {\n        return;\n      } else {\n        attempts++;\n      }\n      try {\n        Thread.sleep(ThreadLocalRandom.current().nextLong(UPDATE_RETRY_DELAY));\n      }\n      catch (InterruptedException ie) {\n        throw new RuntimeException(ie);\n      }\n    }","sourceCodeStart":784,"sourceCodeEnd":820,"githubUrl":"https://github.com/apache/druid/blob/9b90983fd291f26935af934383ce360473179e4d/extensions-core/druid-basic-security/src/main/java/org/apache/druid/security/basic/authorization/db/updater/CoordinatorBasicAuthorizerMetadataStorageUpdater.java#L784-L820","documentation":"Removing a role from a user failed because every compare-and-swap attempt on the user map lost to concurrent writers, exhausting numRetries. The updater re-reads the user map, removes the role, and retries the CAS with randomized delays before throwing this ISE with the role and user names. It signals persistent contention on metadata storage.","triggerScenarios":"Calling unassignUserRole while other clients continuously update the same authorizer prefix's user or role maps so the CAS never succeeds within the retry budget.","commonSituations":"Parallel offboarding/cleanup scripts; simultaneous unassignment and permission edits; write contention between admin UI sessions and automation.","solutions":["Retry unassignUserRole after a short backoff.","Enforce single-writer access to basic-security metadata.","Serialize user offboarding operations.","Increase numRetries to absorb concurrent update bursts.","Check metadata storage performance."],"exampleFix":"// before\nclient.unassignUserRole(prefix, userName, roleName);\n// after\nfor (int i = 0; i < 5; i++) {\n  try { client.unassignUserRole(prefix, userName, roleName); return; }\n  catch (IJSE e) { Thread.sleep(1000); }\n}","handlingStrategy":"retry","validationCode":"// Skip if user doesn't have the role\nBasicAuthorizerUser u = BasicAuthUtils.deserializeAuthorizerUserMap(\n    mapper, getCurrentUserMapBytes(prefix)).get(userName);\nif (u == null || !u.getRoles().contains(roleName)) return;","typeGuard":null,"tryCatchPattern":"try {\n  updater.unassignUserRole(prefix, userName, roleName);\n} catch (IJSE e) {\n  await.atMost(Duration.ofSeconds(10)).untilAsserted(\n      () -> updater.unassignUserRole(prefix, userName, roleName));\n}","preventionTips":["Serialize offboarding workflows","Single-writer metadata updates","Backoff between unassignment retries","Tune numRetries to expected concurrency","Check metadata store latency"],"tags":["druid","basic-security","zookeeper","concurrency"],"backgroundTag":"concurrent-update-contention","analyzedSha":"9b90983fd291f26935af934383ce360473179e4d","analyzedAt":"2026-09-07T13:32:30.957Z","contentChangedAt":"2026-09-07T13:32:30.957Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}