Skip to content

Fix concurrent group access to prevent NullPointerException#1

Open
akshayutture-augment wants to merge 1 commit into
feature-group-concurrency-updatefrom
feature-group-concurrency-implementation
Open

Fix concurrent group access to prevent NullPointerException#1
akshayutture-augment wants to merge 1 commit into
feature-group-concurrency-updatefrom
feature-group-concurrency-implementation

Conversation

@akshayutture-augment

@akshayutture-augment akshayutture-augment commented Nov 14, 2025

Copy link
Copy Markdown

Test 10

Summary by CodeRabbit

  • Bug Fixes

    • Enhanced null-safety handling in group subgroup count operations to prevent potential errors.
  • Tests

    • Added concurrent group operations test to validate system stability during simultaneous create, delete, and read operations.

✏️ Tip: You can customize this high-level summary in your review settings.


Replicated from ai-code-review-evaluation/keycloak-coderabbit#10

Closes #40368

Signed-off-by: vramik <vramik@redhat.com>
@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 23, 2025

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Nov 23, 2025

Copy link
Copy Markdown

Walkthrough

This pull request refactors group caching logic by introducing lazy supplier evaluation with null-safety in GroupAdapter, adds an explicit @Override annotation to CachedGroup, removes an unused helper method from GroupUtils, and introduces a concurrent read-write test for group operations.

Changes

Cohort / File(s) Summary
Group Cache Refactoring
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
Modified getSubGroupsCount() to use lazy supplier evaluation (modelSupplier.get()) instead of direct getGroupModel() retrieval, with null-safe handling to prevent NPE
Cache Entity Enhancement
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java
Added @Override annotation to getRealm() method, explicitly indicating override of parent method
Utilities Cleanup
services/src/main/java/org/keycloak/utils/GroupUtils.java
Removed private helper method groupMatchesSearchOrIsPathElement()
Concurrency Testing
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java
Added new test method createMultiDeleteMultiReadMulti() that validates concurrent group read operations during deletion using multi-threaded patterns; added supporting imports (Matchers, CopyOnWriteArrayList, AtomicBoolean, IntStream, fail)

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

  • Verify that groupMatchesSearchOrIsPathElement() removal from GroupUtils has no external callers or dependencies
  • Review the concurrent test logic to ensure proper synchronization and exception capture semantics
  • Confirm null-safety pattern in GroupAdapter supplier evaluation aligns with codebase conventions

Poem

🐰 Lazy suppliers hop so spry,
Null checks catch what once flew by,
Old helpers rest, their job now done,
New tests race—read, delete, all fun!
With @Override crowns held high,
Our groups dance safely through the sky!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is minimal and lacks substantive details about the changes, objectives, or context required by standard contribution guidelines. Expand the description to explain the problem being solved, the implementation approach, testing performed, and any relevant context or related issues.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main objective of the changes: addressing concurrent group access issues and preventing NullPointerException through lazy loading and null-safe handling.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature-group-concurrency-implementation

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (1)

144-147: Remove unnecessary blank line in exception handler.

The empty line 145 between the catch block and adding the exception serves no purpose.

             } catch (Exception e) {
-
                 caughtExceptions.add(e);
             }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e92b825 and 332c9b6.

📒 Files selected for processing (4)
  • model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java (1 hunks)
  • model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java (1 hunks)
  • services/src/main/java/org/keycloak/utils/GroupUtils.java (0 hunks)
  • tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (4 hunks)
💤 Files with no reviewable changes (1)
  • services/src/main/java/org/keycloak/utils/GroupUtils.java
🔇 Additional comments (2)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java (1)

61-61: LGTM! Good use of @OverRide annotation.

The explicit @Override annotation improves code clarity and enables compile-time verification that this method correctly overrides a method from the InRealm interface.

model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java (1)

274-275: Excellent concurrent access protection!

The null-safe handling prevents NullPointerException when the group is deleted or invalidated during concurrent access. Returning null appropriately signals that the count is unavailable rather than throwing an exception.

Comment on lines +118 to +158
@Test
public void createMultiDeleteMultiReadMulti() {
// create multiple groups
List<String> groupUuuids = new ArrayList<>();
IntStream.range(0, 100).forEach(groupIndex -> {
GroupRepresentation group = new GroupRepresentation();
group.setName("Test Group " + groupIndex);
try (Response response = managedRealm.admin().groups().add(group)) {
boolean created = response.getStatusInfo().getFamily() == Response.Status.Family.SUCCESSFUL;
if (created) {
final String groupUuid = ApiUtil.getCreatedId(response);
groupUuuids.add(groupUuid);
} else {
fail("Failed to create group: " + response.getStatusInfo().getReasonPhrase());
}
}
});

AtomicBoolean deletedAll = new AtomicBoolean(false);
List<Exception> caughtExceptions = new CopyOnWriteArrayList<>();
// read groups in a separate thread
new Thread(() -> {
while (!deletedAll.get()) {
try {
// just loading briefs
managedRealm.admin().groups().groups(null, 0, Integer.MAX_VALUE, true);
} catch (Exception e) {

caughtExceptions.add(e);
}
}
}).start();

// delete groups
groupUuuids.forEach(groupUuid -> {
managedRealm.admin().groups().group(groupUuid).remove();
});
deletedAll.set(true);

assertThat(caughtExceptions, Matchers.empty());
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add thread synchronization to ensure reliable test execution.

The test has several concurrency concerns:

  1. Missing thread join: The background thread may still be executing when the assertion runs. Add Thread.join() after setting deletedAll to ensure the thread completes.

  2. Race condition: If deletions complete very quickly, the background thread might not execute any iterations before deletedAll becomes true, making the test ineffective at verifying concurrent access.

  3. No verification of actual concurrent access: There's no guarantee the thread actually performed any reads during the deletion window.

Apply this refactor to fix the synchronization issues:

     AtomicBoolean deletedAll = new AtomicBoolean(false);
     List<Exception> caughtExceptions = new CopyOnWriteArrayList<>();
+    AtomicBoolean threadStarted = new AtomicBoolean(false);
     // read groups in a separate thread
-    new Thread(() -> {
+    Thread readerThread = new Thread(() -> {
+        threadStarted.set(true);
         while (!deletedAll.get()) {
             try {
                 // just loading briefs
                 managedRealm.admin().groups().groups(null, 0, Integer.MAX_VALUE, true);
             } catch (Exception e) {
-
                 caughtExceptions.add(e);
             }
         }
-    }).start();
+    });
+    readerThread.start();
+    
+    // Wait for reader thread to start
+    while (!threadStarted.get()) {
+        Thread.sleep(10);
+    }

     // delete groups
     groupUuuids.forEach(groupUuid -> {
         managedRealm.admin().groups().group(groupUuid).remove();
     });
     deletedAll.set(true);
+    
+    // Wait for reader thread to complete
+    readerThread.join();

     assertThat(caughtExceptions, Matchers.empty());

Note: This requires adding throws InterruptedException to the test method signature or wrapping in a try-catch block.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java around
lines 118 to 158, the background reader thread is not synchronized with the main
thread causing unreliable test behavior; modify the test to create and start the
reader thread, use a synchronization primitive (CountDownLatch or Phaser) to
ensure the reader thread has started and performed at least one read before
deletions begin, after setting deletedAll = true call thread.join() (or await on
the latch) to guarantee the reader exits before assertions run, and either add
throws InterruptedException to the test signature or catch InterruptedException
around join/await; also track a simple counter/flag updated by the reader to
assert that at least one read occurred during deletions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants