From 8377b61404033c479afa38424211314e84c2105a Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 06 Aug 2026 08:03:21 +0000
Subject: [PATCH] Replace per-operation backend read lock with a scalable shared-access gate (#680)
---
opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++----
1 files changed, 104 insertions(+), 8 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
index 94d5f2b..c258484 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/backends/pluggable/EntryContainer.java
@@ -236,7 +236,7 @@
{
final ConfigChangeResult ccr = new ConfigChangeResult();
- exclusiveLock.lock();
+ EntryContainer.this.lock();
try
{
storage.write(new WriteOperation()
@@ -256,7 +256,7 @@
}
finally
{
- exclusiveLock.unlock();
+ EntryContainer.this.unlock();
}
return ccr;
@@ -318,7 +318,7 @@
public ConfigChangeResult applyConfigurationDelete(final BackendVLVIndexCfg cfg)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
- exclusiveLock.lock();
+ EntryContainer.this.lock();
try
{
storage.write(new WriteOperation()
@@ -337,7 +337,7 @@
}
finally
{
- exclusiveLock.unlock();
+ EntryContainer.this.unlock();
}
return ccr;
}
@@ -348,6 +348,73 @@
final Lock sharedLock = lock.readLock();
final Lock exclusiveLock = lock.writeLock();
+ /**
+ * Striped count of in-flight lock-free shared accesses. Every operation
+ * (search, bind, compare, modify, ...) enters the entry container through
+ * {@link #beginSharedAccess()}, so acquiring even the read side of the
+ * ReentrantReadWriteLock becomes a cross-core hotspot under load: each
+ * acquire and release CAS-es the single lock state word. The hot paths
+ * register through this striped counter instead and only fall back to
+ * waiting when an exclusive locker has closed the gate; exclusive lockers
+ * (rare structural changes: index removal, configuration changes, close)
+ * close the gate through {@link #lock()} and drain in-flight accesses. The
+ * drain relies on {@link StripedCounter#sum()} never under-counting to a
+ * false zero, which is why this is not a LongAdder — see StripedCounter.
+ */
+ private final StripedCounter sharedAccessCount = new StripedCounter();
+ /** True while an exclusive locker has closed the gate for lock-free shared access. */
+ private volatile boolean exclusiveAccessPending;
+ /** Monitor used to park shared accessors while the gate is closed. */
+ private final Object sharedAccessMonitor = new Object();
+
+ /**
+ * Begins a lock-free shared access to this entry container. Must be paired
+ * with {@link #endSharedAccess()} in a finally block on the same thread.
+ * Equivalent to acquiring {@link #sharedLock}, but scales with the number
+ * of cores.
+ */
+ void beginSharedAccess()
+ {
+ boolean interrupted = false;
+ for (;;)
+ {
+ sharedAccessCount.increment();
+ if (!exclusiveAccessPending)
+ {
+ break;
+ }
+ // An exclusive locker is active or draining: back out and wait.
+ sharedAccessCount.decrement();
+ synchronized (sharedAccessMonitor)
+ {
+ while (exclusiveAccessPending)
+ {
+ try
+ {
+ sharedAccessMonitor.wait();
+ }
+ catch (InterruptedException e)
+ {
+ interrupted = true;
+ }
+ }
+ }
+ }
+ if (interrupted)
+ {
+ Thread.currentThread().interrupt();
+ }
+ }
+
+ /**
+ * Ends a lock-free shared access to this entry container. Must be called by
+ * the thread that did the paired {@link #beginSharedAccess()}.
+ */
+ void endSharedAccess()
+ {
+ sharedAccessCount.decrement();
+ }
+
EntryContainer(DN baseDN, String backendID, PluggableBackendCfg config, Storage storage, RootContainer rootContainer,
ServerContext serverContext) throws ConfigException
{
@@ -2412,7 +2479,7 @@
{
final ConfigChangeResult ccr = new ConfigChangeResult();
- exclusiveLock.lock();
+ EntryContainer.this.lock();
try
{
storage.write(new WriteOperation()
@@ -2436,7 +2503,7 @@
}
finally
{
- exclusiveLock.unlock();
+ EntryContainer.this.unlock();
}
return ccr;
@@ -2731,15 +2798,44 @@
searchOp.addResponseControl(new VLVResponseControl(targetPosition, contentCount, vlvResultCode));
}
- /** Get the exclusive lock. */
+ /**
+ * Get the exclusive lock: acquires the write lock (excluding legacy
+ * sharedLock readers), closes the gate for lock-free shared accessors and
+ * drains the in-flight ones.
+ */
void lock()
{
exclusiveLock.lock();
+ exclusiveAccessPending = true;
+ // The drain must not be abandoned on interrupt: returning early would let
+ // the exclusive caller run concurrently with in-flight shared accesses.
+ // Exclusive lockers are rare, so sleep-polling is an acceptable trade-off.
+ boolean interrupted = false;
+ while (sharedAccessCount.sum() != 0)
+ {
+ try
+ {
+ Thread.sleep(1);
+ }
+ catch (InterruptedException e)
+ {
+ interrupted = true;
+ }
+ }
+ if (interrupted)
+ {
+ Thread.currentThread().interrupt();
+ }
}
- /** Unlock the exclusive lock. */
+ /** Unlock the exclusive lock and reopen the gate for lock-free shared accessors. */
void unlock()
{
+ exclusiveAccessPending = false;
+ synchronized (sharedAccessMonitor)
+ {
+ sharedAccessMonitor.notifyAll();
+ }
exclusiveLock.unlock();
}
--
Gitblit v1.10.0