Skip to content

OAK-12341: Thread Contention authentication.external.impl.principal.SyncConfigTracker - #3072

Closed
jsedding wants to merge 3 commits into
trunkfrom
OAK-12341-synchandler-contention
Closed

OAK-12341: Thread Contention authentication.external.impl.principal.SyncConfigTracker#3072
jsedding wants to merge 3 commits into
trunkfrom
OAK-12341-synchandler-contention

Conversation

@jsedding

Copy link
Copy Markdown
Contributor

I propose this fix as an alternative to PR #3069. I took the unit-test from the original PR, made it fast, and implemented a fix that relies on an internally cached field of ServiceTracker.

cc @pat-lego

patlego and others added 3 commits August 10, 2026 09:36
@github-actions

Copy link
Copy Markdown

Commit-Check ❌

Commit rejected by Commit-Check.                                  
                                                                  
  (c).-.(c)    (c).-.(c)    (c).-.(c)    (c).-.(c)    (c).-.(c)  
   / ._. \      / ._. \      / ._. \      / ._. \      / ._. \   
 __\( C )/__  __\( H )/__  __\( E )/__  __\( C )/__  __\( K )/__ 
(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)(_.-/'-'\-._)
   || E ||      || R ||      || R ||      || O ||      || R ||   
 _.' '-' '._  _.' '-' '._  _.' '-' '._  _.' '-' '._  _.' '-' '._ 
(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)(.-./`-´\.-.)
 `-´     `-´  `-´     `-´  `-´     `-´  `-´     `-´  `-´     `-´ 
                                                                  
Commit rejected.                                                  
                                                                  
Type message check failed ==> Test to showcase blocking operation creating thread contention during high load 
The commit message should follow Conventional Commits. See https://www.conventionalcommits.org
Suggest: Commit message does not match the required pattern

@sonarqubecloud

Copy link
Copy Markdown

@pat-lego

Copy link
Copy Markdown
Contributor

Wouldnt the issue still be present in this scenario, we really dont want to call getServiceReferences, in this scenario we still call it and thats what leads to the blocking operation. In the case of a large burst of requests this lookup can cause hundreds of threads to become a FIFO like queue.

In my PR what I propose is what we let the OSGi framework push the services to us and we track them avoiding the lookup completely.

@jsedding

Copy link
Copy Markdown
Contributor Author

@pat-lego I am calling getServiceReference() (singular, not plural). This caches the first reference in a field of the ServiceTracker class.

BTW, I included your test case and my change passes it. I like how you're checking for contention! 👍

@pat-lego

Copy link
Copy Markdown
Contributor
 private ServiceReference<SyncHandler>[] getReferences() {
        ServiceReference<SyncHandler>[] refs = getServiceReferences();

Yes sorry I should be more specific, is there ever a reason we should also fix getReferences to not call getServiceReferences to prevent this condition. I know in the thread dump its isEnabled -> getReferences which this PR fixes that flow but the question is should we fix getReferences as well.

@rishabhdaim rishabhdaim changed the title OAK-12341 - Thread Contention authentication.external.impl.principal.SyncConfigTracker OAK-12341 : Thread Contention authentication.external.impl.principal.SyncConfigTracker Aug 12, 2026
*/
boolean isEnabled() {
return getReferences().length > 0;
return getServiceReference() != null;

@rishabhdaim rishabhdaim Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This only makes the isEnabled() check itself lock-free. Every caller that actually gets past that check — AutomembershipService/DynamicGroupMembershipService building an AutoMembershipProvider/ExternalGroupPrincipalProvider — immediately calls getAutoMembership(), hasDynamicGroupsEnabled(), getGroupAutoMembership(), getAutoMembershipConfig(), getIdpNamesWithDynamicGroups(), all of which still call getServiceReferences() and still synchronize on the same Tracked monitor.

So for real requests (anywhere isEnabled() is true, which is the whole point of this code existing) the contention from the reported thread dump doesn't actually go away — it's only fixed for the isolated isEnabled()-only case this PR's test exercises. This is the same thing @pat-lego asked above :

should getReferences() get the same treatment?

+ "the shared Tracked monitor, as observed in the OAK-12341 thread dump", blockedMs > 0);
}

/**

@rishabhdaim rishabhdaim Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This javadoc doesn't match what the fix actually does — there's no CopyOnWriteArrayList and no addingService/removedService override anywhere in SyncConfigTracker. The real mechanism is the inherited ServiceTracker.getServiceReference() (singular), which caches the resolved reference internally and only falls back to the synchronized getServiceReferences() scan on a cache miss — like @jsedding described above. Worth fixing so a future reader doesn't go looking for a cache field that isn't there.


long[] threads = threadIds.stream().mapToLong(Long::longValue).toArray();
long blockedMs = 0;
for (int i = 0; i < 1000; i++) {

@rishabhdaim rishabhdaim Aug 12, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This assertion (blockedMs > 0) depends on a single spinner thread resubmitting one-shot tasks through a bounded queue, rather than all THREAD_COUNT threads hammering concurrently — real overlap only happens once the queue fills, which isn't guaranteed on a fast or CPU-constrained CI box. Might be worth having each worker loop directly instead of resubmitting through pool.execute(), and polling against a wall-clock timeout instead of a fixed 1000 iterations, so this doesn't flake.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Polling against a wall clock was in place before. One assertion checks that the threads are not blocked, so it always waits for the wall clock duration. If we can set the wall clock to 50ms or less, that's fine.

@rishabhdaim

rishabhdaim commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

commit-check is actually failing here — not for the "Conventional Commits" reason the bot claims, but because this repo's own regex (.github/cchk.toml) requires commit subjects to start with OAK-<digits>, and one of these three commits doesn't. Worth rewording that commit before merge.

@rishabhdaim rishabhdaim changed the title OAK-12341 : Thread Contention authentication.external.impl.principal.SyncConfigTracker OAK-12341: Thread Contention authentication.external.impl.principal.SyncConfigTracker Aug 12, 2026
@jsedding

Copy link
Copy Markdown
Contributor Author

Closing in favour of #3069. Apparently, the contention does not only affect the isEnabled() code path.

@jsedding jsedding closed this Aug 14, 2026
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.

3 participants