Skip to content

Fix concurrent group access to prevent NullPointerException#10

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

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

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Jan 5, 2026

Copy link
Copy Markdown
Collaborator

Test 10

Summary by CodeRabbit

Release Notes

  • Tests

    • Added comprehensive concurrent group operations test to validate reliability during simultaneous create, read, and delete operations.
  • Bug Fixes

    • Improved group caching stability with enhanced null-safety checks.

✏️ 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>
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 12, 2026

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 Jan 12, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This pull request refactors the Keycloak group caching layer by modifying null-safe retrieval in GroupAdapter, adding override annotation clarity in CachedGroup, removing an unused private utility method from GroupUtils, and introducing a new concurrent stress test for group operations.

Changes

Cohort / File(s) Summary
Cache Infrastructure
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java, model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java
GroupAdapter now retrieves sub-groups count via lazy-loaded modelSupplier with explicit null handling instead of direct getGroupModel() delegation. CachedGroup adds @Override annotation to getRealm() for override clarity.
Utility Cleanup
services/src/main/java/org/keycloak/utils/GroupUtils.java
Removed private helper method groupMatchesSearchOrIsPathElement() that handled group name matching and path element search logic.
Testing
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java
Added new concurrent integration test createMultiDeleteMultiReadMulti() that creates 100 groups, reads them in a background thread concurrently with deletion operations, then verifies no exceptions occurred during concurrent access.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~18 minutes

Poem

🐰 Cache suppliers spin their web,
null checks flowing, ebbing tides of dread,
concurrent readers hop with glee,
old helpers rest in history,
cleaner warren, forward thread! 🌿

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The pull request description is minimal and lacks substantive details about the changes, their rationale, and testing methodology beyond a test label. Expand the description with details about the NullPointerException issue, the changes made to fix it, affected components, and validation approach.
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix concurrent group access to prevent NullPointerException' accurately describes the main objective of the pull request, which addresses concurrent access issues in group operations.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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

@AI-Code-Review-Evals AI-Code-Review-Evals deleted a comment from coderabbitai Bot Jan 12, 2026

@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: 2

🤖 Fix all issues with AI agents
In
@model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java:
- Around line 274-275: In GroupAdapter, make the getSubGroupsStream overloads
null-safe like getSubGroupsCount: call modelSupplier.get() into a local
GroupModel variable, check for null, and if null return an appropriate empty
result (e.g., Stream.empty()) instead of invoking methods on a null reference;
otherwise delegate to model.getSubGroupsStream(...). Ensure you update all three
overloads that currently call modelSupplier.get().getSubGroupsStream(...) so
they use this pattern.

In @tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java:
- Around line 136-158: The reader thread created for calling
managedRealm.admin().groups().groups(...) must be joined before asserting on
caughtExceptions to avoid the race; change the anonymous new Thread(...) to
assign it to a variable (e.g., readerThread), start it, and after
deletedAll.set(true) call readerThread.join() (optionally with a timeout) before
assertThat(caughtExceptions, Matchers.empty()); this ensures the reader loop has
finished and any exceptions have been recorded.
🧹 Nitpick comments (1)
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (1)

118-134: Test setup is appropriate for concurrent stress testing.

The group creation loop with proper error handling and the use of thread-safe collections (CopyOnWriteArrayList, AtomicBoolean) are good choices for this concurrency test. Consider adding cleanup for created groups in case assertions fail mid-test (e.g., using managedRealm.cleanup().add(...) after each successful creation).

📜 Review details

Configuration used: defaults

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
  • model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java
  • services/src/main/java/org/keycloak/utils/GroupUtils.java
  • tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java
💤 Files with no reviewable changes (1)
  • services/src/main/java/org/keycloak/utils/GroupUtils.java
🧰 Additional context used
🧬 Code graph analysis (1)
tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java (2)
tests/utils/src/main/java/org/keycloak/tests/utils/matchers/Matchers.java (1)
  • Matchers (41-166)
docs/documentation/tests/src/test/java/org/keycloak/documentation/test/utils/HttpUtils.java (1)
  • Response (108-146)
🔇 Additional comments (1)
model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/entities/CachedGroup.java (1)

61-64: LGTM!

Adding the @Override annotation improves code clarity and enables compile-time verification that this method correctly implements the interface contract.

Comment on lines +274 to +275
GroupModel model = modelSupplier.get();
return model == null ? null : model.getSubGroupsCount();

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

Null-safety fix is correct, but other similar methods lack the same protection.

The null check here correctly prevents NPE when the group is deleted concurrently. However, the getSubGroupsStream overloads at lines 256, 262, and 268 call modelSupplier.get() directly without null checks:

return modelSupplier.get().getSubGroupsStream(search, firstResult, maxResults);

These would still throw NPE during concurrent deletion. Consider applying the same null-safe pattern consistently.

🔧 Suggested fix for consistency
 @Override
 public Stream<GroupModel> getSubGroupsStream(String search, Integer firstResult, Integer maxResults) {
     if (isUpdated()) return updated.getSubGroupsStream(search, firstResult, maxResults);
-    return modelSupplier.get().getSubGroupsStream(search, firstResult, maxResults);
+    GroupModel model = modelSupplier.get();
+    return model == null ? Stream.empty() : model.getSubGroupsStream(search, firstResult, maxResults);
 }

 @Override
 public Stream<GroupModel> getSubGroupsStream(Integer firstResult, Integer maxResults) {
     if (isUpdated()) return updated.getSubGroupsStream(firstResult, maxResults);
-    return modelSupplier.get().getSubGroupsStream(firstResult, maxResults);
+    GroupModel model = modelSupplier.get();
+    return model == null ? Stream.empty() : model.getSubGroupsStream(firstResult, maxResults);
 }

 @Override
 public Stream<GroupModel> getSubGroupsStream(String search, Boolean exact, Integer firstResult, Integer maxResults) {
     if (isUpdated()) return updated.getSubGroupsStream(search, exact, firstResult, maxResults);
-    return modelSupplier.get().getSubGroupsStream(search, exact, firstResult, maxResults);
+    GroupModel model = modelSupplier.get();
+    return model == null ? Stream.empty() : model.getSubGroupsStream(search, exact, firstResult, maxResults);
 }
🤖 Prompt for AI Agents
In
@model/infinispan/src/main/java/org/keycloak/models/cache/infinispan/GroupAdapter.java
around lines 274 - 275, In GroupAdapter, make the getSubGroupsStream overloads
null-safe like getSubGroupsCount: call modelSupplier.get() into a local
GroupModel variable, check for null, and if null return an appropriate empty
result (e.g., Stream.empty()) instead of invoking methods on a null reference;
otherwise delegate to model.getSubGroupsStream(...). Ensure you update all three
overloads that currently call modelSupplier.get().getSubGroupsStream(...) so
they use this pattern.

Comment on lines +136 to +158
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 | 🟡 Minor

Race condition: reader thread is not joined before assertion.

The test sets deletedAll.set(true) and immediately asserts on caughtExceptions, but the reader thread may still be executing. This can cause flaky results or miss exceptions from the final loop iteration.

🔧 Proposed fix
+        Thread readerThread = new 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();
+        });
+        readerThread.start();

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

+        try {
+            readerThread.join(5000); // Wait up to 5 seconds for reader to finish
+        } catch (InterruptedException e) {
+            Thread.currentThread().interrupt();
+            fail("Reader thread interrupted");
+        }
+
         assertThat(caughtExceptions, Matchers.empty());
🤖 Prompt for AI Agents
In @tests/base/src/test/java/org/keycloak/tests/admin/group/GroupTest.java
around lines 136 - 158, The reader thread created for calling
managedRealm.admin().groups().groups(...) must be joined before asserting on
caughtExceptions to avoid the race; change the anonymous new Thread(...) to
assign it to a variable (e.g., readerThread), start it, and after
deletedAll.set(true) call readerThread.join() (optionally with a timeout) before
assertThat(caughtExceptions, Matchers.empty()); this ensures the reader loop has
finished and any exceptions have been recorded.

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