Make AciList reads lock-free with true copy-on-write#674
Open
vharseko wants to merge 3 commits into
Open
Conversation
AciList.getCandidateAcis() acquired a ReentrantReadWriteLock read lock on every access-controlled operation (compare, search, modify, ...) to walk the ACI map. The field comment claims "copy-on-write technique to avoid locking when reading", but mutators actually modified the volatile DITCacheMap (and its value lists) in place under the write lock, so the read lock could not be dropped. Make the copy-on-write real: mutators (add/remove/rename/modify) build a copy of the map with copied value lists under the write lock and publish it through the volatile reference; a published map is never modified in place. getCandidateAcis() now reads a snapshot lock-free. ACI mutations are rare (startup, backend initialization, ACI modifications), so the copy cost is negligible.
maximthomas
approved these changes
Jul 3, 2026
This was referenced Jul 3, 2026
Replace the ReentrantReadWriteLock with a ReentrantLock — the read lock has no users since reads went lock-free — and document that Aci instances are treated as immutable once published. Add AciListTests, which hammers getCandidateAcis() snapshots from several threads while a writer appends to the list under the queried key, resets it and renames a neighbouring key; validated to fail against an in-place-mutation regression and pass with copy-on-write.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First result of profiling the COMPARE hot path with the same methodology
as the BIND series (#660, #667–#670, #672). The one COMPARE-specific
serialization point vs BIND is access control:
AciList.getCandidateAcis()acquires a
ReentrantReadWriteLockread lock on every access-controlledoperation (compare, search, modify, …) just to walk the ACI map. The field
comment promises "copy-on-write technique to avoid locking when reading",
but the mutators actually modify the volatile
DITCacheMap(and its valuelists) in place under the write lock — so the read lock could never be
dropped. This PR makes the copy-on-write real and removes the read lock from
the hot path — the same pattern as the
CompressedSchemafix (#670).Problem
Every acquire/release CAS-es the shared RRWL sync word — a cross-core
cache-coherency hotspot on the handler singleton, growing with core count.
Change
addAci×3,modAciOldNewEntry,removeAci×2,renameAci) now build a copy of the map with copied value lists(`copyAciList()") under the write lock, modify the copy, and publish it
through the volatile reference. A published map is never modified in
place. ACI mutations are rare (startup, backend initialization, ACI
changes), so the copy cost is negligible.
getCandidateAcis()reads a snapshot lock-free.ReentrantLockserializes mutators (the read/write lock lostits last read-lock user — see the review follow-up below); public
behaviour is unchanged.
Benchmark
COMPARE scenario, packaged server with the bind-series fixes applied,
jebackend, 5,000 users, JDK 11, 8-core host, access loggers disabled. Load
driver: 200 persistent connections bound as regular users (real ACI
evaluation, no root bypass), each issuing
COMPARE uid=user.N / uid / user.Nfor random users; latency via HdrHistogram; 40 s warm-up run afterevery server restart, then 15 s + 75 s measured run under
caffeinate.Interleaved order old→new→new→old→old→new after a cool-down:
Adjacent pairs favor the lock-free version (+8.1%, +2.9%); as with #670 the
local win on 8 cores is modest and the structural value is removing a
serialization ceiling that grows with core count. Errors: 0 in all runs.
Review follow-up (b712f6e)
All three optional review suggestions applied:
ReentrantReadWriteLock→ReentrantLock: the read lock has no userssince reads went lock-free.
aciListfield javadoc thatAciinstances aretreated as immutable once published — the map and its value lists are
the only mutable containers the COW contract covers.
AciListTests, a concurrency regression test that locks in theCOW contract: three reader threads snapshot
getCandidateAcis()continuously while a writer appends to the value list under the
queried key (Entry-based
addAci), resets it and renames aneighbouring key (50k iterations); readers assert every snapshot is
consistent and exception-free. The detector was validated in both
directions: with
copyAciList()stubbed to return the live map(simulated in-place-mutation regression) the test fails, with real COW
it passes. Put-only and rename-only churn variants were found NOT to
catch the regression (same-key put is structure-neutral and rename
churns unrelated hash buckets), which is why the append path is the
primary channel.
Testing
AciTests(515 tests,TestNG group
slow, excluded from the default run) produces identicalresults before and after the change — 513 pass, plus the same 2
pre-existing failures (
testCompare,testValidAcis) that also fail onunmodified master (worth a separate issue; this suite is never exercised
in CI).
CompareOperationTestCase(37),GetEffectiveRightsTestCase(7) — pass.AciListTests+AciBodyTest+GetEffectiveRightsTestCase— 15 tests, 0 failures.Files
opendj-server-legacy/src/main/java/org/opends/server/authorization/dseecompat/AciList.javaopendj-server-legacy/src/test/java/org/opends/server/authorization/dseecompat/AciListTests.java(new)