-
Notifications
You must be signed in to change notification settings - Fork 0
Fix concurrent group access to prevent NullPointerException #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: feature-group-concurrency-update
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ | |
| import org.apache.http.client.methods.CloseableHttpResponse; | ||
| import org.apache.http.client.methods.HttpGet; | ||
| import org.apache.http.impl.client.CloseableHttpClient; | ||
| import org.hamcrest.Matchers; | ||
| import org.junit.jupiter.api.Assertions; | ||
| import org.junit.jupiter.api.Test; | ||
| import org.keycloak.admin.client.Keycloak; | ||
|
|
@@ -76,6 +77,9 @@ | |
| import java.util.List; | ||
| import java.util.Map; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.CopyOnWriteArrayList; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.stream.IntStream; | ||
|
|
||
| import static org.hamcrest.MatcherAssert.assertThat; | ||
| import static org.hamcrest.Matchers.anEmptyMap; | ||
|
|
@@ -90,6 +94,7 @@ | |
| import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| import static org.junit.jupiter.api.Assertions.assertNull; | ||
| import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| import static org.junit.jupiter.api.Assertions.fail; | ||
|
|
||
| /** | ||
| * @author <a href="mailto:mstrukel@redhat.com">Marko Strukelj</a> | ||
|
|
@@ -109,6 +114,49 @@ public class GroupTest extends AbstractGroupTest { | |
| @InjectHttpClient | ||
| CloseableHttpClient httpClient; | ||
|
|
||
|
|
||
| @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()); | ||
| } | ||
|
Comment on lines
+136
to
+158
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Race condition: reader thread is not joined before assertion. The test sets 🔧 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 |
||
|
|
||
| // KEYCLOAK-2716 Can't delete client if its role is assigned to a group | ||
| @Test | ||
| public void testClientRemoveWithClientRoleGroupMapping() { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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
getSubGroupsStreamoverloads at lines 256, 262, and 268 callmodelSupplier.get()directly without null checks: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