From 51ed0a46ef2b6382d87ce8603c65b6d88aead7ba Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Thu, 06 Aug 2026 11:31:05 +0000
Subject: [PATCH] Remove per-bind global lock contention in AuthenticatedUsers (#660)
---
opendj-server-legacy/src/test/java/org/opends/server/core/BindLatencyBenchmarkTestCase.java | 403 +++++++++++++++++++++++++++++++++
opendj-server-legacy/src/main/java/org/opends/server/core/AuthenticatedUsers.java | 273 +++++++++------------
opendj-server-legacy/src/test/java/org/opends/server/types/PrivilegeTestCase.java | 3
3 files changed, 522 insertions(+), 157 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/core/AuthenticatedUsers.java b/opendj-server-legacy/src/main/java/org/opends/server/core/AuthenticatedUsers.java
index 33461b6..0eca4f8 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/core/AuthenticatedUsers.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/core/AuthenticatedUsers.java
@@ -13,21 +13,22 @@
*
* Copyright 2008-2010 Sun Microsystems, Inc.
* Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.core;
import java.util.EnumSet;
import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
import java.util.Set;
-import java.util.concurrent.CopyOnWriteArraySet;
-import java.util.concurrent.locks.ReentrantReadWriteLock;
+import java.util.concurrent.ConcurrentHashMap;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.api.ClientConnection;
-import org.opends.server.api.DITCacheMap;
import org.opends.server.api.plugin.InternalDirectoryServerPlugin;
import org.opends.server.api.plugin.PluginResult.PostResponse;
import org.opends.server.types.DisconnectReason;
@@ -48,6 +49,17 @@
* This class also provides a mechanism for detecting changes to authenticated
* user entries and notifying the corresponding client connections so that they
* can update their cached versions.
+ * <BR><BR>
+ * The user map is a {@link ConcurrentHashMap}, so registering and deregistering
+ * a connection (which happens on every bind and unbind) is lock-free at the map
+ * level and only contends at the granularity of a single hash bin. Each value is
+ * a concurrent set with O(1) add/remove, so many connections authenticating as
+ * the same user (e.g. an application service account) do not degrade: a
+ * copy-on-write set here would copy the whole connection array under the bin
+ * lock on every bind. The subtree operations triggered by changes to
+ * authenticated user entries (delete / modify / modify DN) are rare and scan
+ * the key set, which the concurrent map supports with weakly-consistent
+ * iteration.
*/
public class AuthenticatedUsers extends InternalDirectoryServerPlugin
{
@@ -57,10 +69,7 @@
* The mapping between authenticated user DNs and the associated client
* connection objects.
*/
- private final DITCacheMap<CopyOnWriteArraySet<ClientConnection>> userMap;
-
- /** Lock to protect internal data structures. */
- private final ReentrantReadWriteLock lock;
+ private final ConcurrentHashMap<DN, Set<ClientConnection>> userMap;
/** Dummy configuration DN. */
private static final String CONFIG_DN = "cn=Authenticated Users,cn=config";
@@ -75,8 +84,7 @@
// can not be authenticated as a user that does not exist yet.
POST_RESPONSE_MODIFY, POST_RESPONSE_MODIFY_DN, POST_RESPONSE_DELETE),
true);
- userMap = new DITCacheMap<>();
- lock = new ReentrantReadWriteLock();
+ userMap = new ConcurrentHashMap<>();
DirectoryServer.registerInternalPlugin(this);
}
@@ -91,25 +99,19 @@
*/
public void put(DN userDN, ClientConnection clientConnection)
{
- lock.writeLock().lock();
- try
+ // The add must happen inside compute(), under the same bin lock as
+ // remove(): with computeIfAbsent(..).add(..) a concurrent remove() could
+ // unmap the set between the two calls and the connection would be
+ // registered in a set no longer reachable from the map.
+ userMap.compute(userDN, (dn, connectionSet) ->
{
- CopyOnWriteArraySet<ClientConnection> connectionSet = userMap.get(userDN);
if (connectionSet == null)
{
- connectionSet = new CopyOnWriteArraySet<>();
- connectionSet.add(clientConnection);
- userMap.put(userDN, connectionSet);
+ connectionSet = ConcurrentHashMap.newKeySet();
}
- else
- {
- connectionSet.add(clientConnection);
- }
- }
- finally
- {
- lock.writeLock().unlock();
- }
+ connectionSet.add(clientConnection);
+ return connectionSet;
+ });
}
@@ -124,23 +126,11 @@
*/
public void remove(DN userDN, ClientConnection clientConnection)
{
- lock.writeLock().lock();
- try
+ userMap.computeIfPresent(userDN, (k, connectionSet) ->
{
- CopyOnWriteArraySet<ClientConnection> connectionSet = userMap.get(userDN);
- if (connectionSet != null)
- {
- connectionSet.remove(clientConnection);
- if (connectionSet.isEmpty())
- {
- userMap.remove(userDN);
- }
- }
- }
- finally
- {
- lock.writeLock().unlock();
- }
+ connectionSet.remove(clientConnection);
+ return connectionSet.isEmpty() ? null : connectionSet;
+ });
}
@@ -156,42 +146,27 @@
* @return The set of client connections authenticated as the specified user,
* or {@code null} if there are none.
*/
- public CopyOnWriteArraySet<ClientConnection> get(DN userDN)
+ public Set<ClientConnection> get(DN userDN)
{
- lock.readLock().lock();
- try
- {
- return userMap.get(userDN);
- }
- finally
- {
- lock.readLock().unlock();
- }
+ return userMap.get(userDN);
}
@Override
public PostResponse doPostResponse(PostResponseDeleteOperation op)
{
final DN entryDN = op.getEntryDN();
- if (op.getResultCode() != ResultCode.SUCCESS || operationDoesNotTargetAuthenticatedUser(entryDN))
+ if (op.getResultCode() != ResultCode.SUCCESS || userMap.isEmpty())
{
return PostResponse.continueOperationProcessing();
}
- // Identify any client connections that may be authenticated
- // or authorized as the user whose entry has been deleted and terminate them
- Set<CopyOnWriteArraySet<ClientConnection>> arraySet = new HashSet<>();
- lock.writeLock().lock();
- try
- {
- userMap.removeSubtree(entryDN, arraySet);
- }
- finally
- {
- lock.writeLock().unlock();
- }
+ // Identify any client connections that may be authenticated or authorized as
+ // the user whose entry has been deleted (or, for a subtree delete, any user
+ // below it) and terminate them. A single removeSubtree pass both detects and
+ // collects the matches, so no separate pre-check scan is needed.
+ Set<Set<ClientConnection>> arraySet = removeSubtree(entryDN);
- for (CopyOnWriteArraySet<ClientConnection> connectionSet : arraySet)
+ for (Set<ClientConnection> connectionSet : arraySet)
{
for (ClientConnection conn : connectionSet)
{
@@ -202,54 +177,52 @@
return PostResponse.continueOperationProcessing();
}
- private boolean operationDoesNotTargetAuthenticatedUser(final DN entryDN)
+ /**
+ * Removes and returns every connection set whose user DN is at or below the
+ * provided base DN.
+ */
+ private Set<Set<ClientConnection>> removeSubtree(DN baseDN)
{
- lock.readLock().lock();
- try
+ Set<Set<ClientConnection>> removed = new HashSet<>();
+ for (Iterator<Map.Entry<DN, Set<ClientConnection>>> it = userMap.entrySet().iterator();
+ it.hasNext();)
{
- return !userMap.containsSubtree(entryDN);
+ Map.Entry<DN, Set<ClientConnection>> entry = it.next();
+ if (entry.getKey().isSubordinateOrEqualTo(baseDN))
+ {
+ removed.add(entry.getValue());
+ it.remove();
+ }
}
- finally
- {
- lock.readLock().unlock();
- }
+ return removed;
}
@Override
public PostResponse doPostResponse(PostResponseModifyOperation op)
{
final Entry oldEntry = op.getCurrentEntry();
- if (op.getResultCode() != ResultCode.SUCCESS || oldEntry == null
- || operationDoesNotTargetAuthenticatedUser(oldEntry.getName()))
+ if (op.getResultCode() != ResultCode.SUCCESS || oldEntry == null)
{
return PostResponse.continueOperationProcessing();
}
- // Identify any client connections that may be authenticated
- // or authorized as the user whose entry has been modified
- // and update them with the latest version of the entry
- // including any virtual attributes.
- lock.writeLock().lock();
- try
+ // A modify only changes the target entry itself, never its descendants, so an
+ // exact-DN lookup is sufficient (no subtree scan). Identify any client
+ // connections authenticated or authorized as that user and update them with
+ // the latest version of the entry, including any virtual attributes.
+ Set<ClientConnection> connectionSet = userMap.get(oldEntry.getName());
+ if (connectionSet != null)
{
- CopyOnWriteArraySet<ClientConnection> connectionSet = userMap.get(oldEntry.getName());
- if (connectionSet != null)
+ Entry newEntry = null;
+ for (ClientConnection conn : connectionSet)
{
- Entry newEntry = null;
- for (ClientConnection conn : connectionSet)
+ if (newEntry == null)
{
- if (newEntry == null)
- {
- newEntry = op.getModifiedEntry().duplicate(true);
- }
- conn.updateAuthenticationInfo(oldEntry, newEntry);
+ newEntry = op.getModifiedEntry().duplicate(true);
}
+ conn.updateAuthenticationInfo(oldEntry, newEntry);
}
}
- finally
- {
- lock.writeLock().unlock();
- }
return PostResponse.continueOperationProcessing();
}
@@ -259,7 +232,7 @@
final Entry oldEntry = op.getOriginalEntry();
final Entry newEntry = op.getUpdatedEntry();
if (op.getResultCode() != ResultCode.SUCCESS || oldEntry == null || newEntry == null
- || operationDoesNotTargetAuthenticatedUser(oldEntry.getName()))
+ || userMap.isEmpty())
{
return PostResponse.continueOperationProcessing();
}
@@ -270,81 +243,71 @@
// Identify any client connections that may be authenticated
// or authorized as the user whose entry has been modified
// and update them with the latest version of the entry.
- lock.writeLock().lock();
- try
+ final Set<Set<ClientConnection>> arraySet = removeSubtree(oldEntry.getName());
+ for (Set<ClientConnection> connectionSet : arraySet)
{
- final Set<CopyOnWriteArraySet<ClientConnection>> arraySet = new HashSet<>();
- userMap.removeSubtree(oldEntry.getName(), arraySet);
- for (CopyOnWriteArraySet<ClientConnection> connectionSet : arraySet)
+ DN authNDN = null;
+ DN authZDN = null;
+ DN newAuthNDN = null;
+ DN newAuthZDN = null;
+ Set<ClientConnection> newAuthNSet = null;
+ Set<ClientConnection> newAuthZSet = null;
+ for (ClientConnection conn : connectionSet)
{
- DN authNDN = null;
- DN authZDN = null;
- DN newAuthNDN = null;
- DN newAuthZDN = null;
- CopyOnWriteArraySet<ClientConnection> newAuthNSet = null;
- CopyOnWriteArraySet<ClientConnection> newAuthZSet = null;
- for (ClientConnection conn : connectionSet)
+ if (authNDN == null)
{
- if (authNDN == null)
+ authNDN = conn.getAuthenticationInfo().getAuthenticationDN();
+ try
{
- authNDN = conn.getAuthenticationInfo().getAuthenticationDN();
- try
- {
- newAuthNDN = authNDN.rename(oldDN, newDN);
- }
- catch (Exception e)
- {
- // Should not happen.
- logger.traceException(e);
- }
+ newAuthNDN = authNDN.rename(oldDN, newDN);
}
- if (authZDN == null)
+ catch (Exception e)
{
- authZDN = conn.getAuthenticationInfo().getAuthorizationDN();
- try
- {
- newAuthZDN = authZDN.rename(oldDN, newDN);
- }
- catch (Exception e)
- {
- // Should not happen.
- logger.traceException(e);
- }
- }
- if (newAuthNDN != null && authNDN != null && authNDN.isSubordinateOrEqualTo(oldEntry.getName()))
- {
- if (newAuthNSet == null)
- {
- newAuthNSet = new CopyOnWriteArraySet<>();
- }
- conn.getAuthenticationInfo().setAuthenticationDN(newAuthNDN);
- newAuthNSet.add(conn);
- }
- if (newAuthZDN != null && authZDN != null && authZDN.isSubordinateOrEqualTo(oldEntry.getName()))
- {
- if (newAuthZSet == null)
- {
- newAuthZSet = new CopyOnWriteArraySet<>();
- }
- conn.getAuthenticationInfo().setAuthorizationDN(newAuthZDN);
- newAuthZSet.add(conn);
+ // Should not happen.
+ logger.traceException(e);
}
}
- if (newAuthNDN != null && newAuthNSet != null)
+ if (authZDN == null)
{
- userMap.put(newAuthNDN, newAuthNSet);
+ authZDN = conn.getAuthenticationInfo().getAuthorizationDN();
+ try
+ {
+ newAuthZDN = authZDN.rename(oldDN, newDN);
+ }
+ catch (Exception e)
+ {
+ // Should not happen.
+ logger.traceException(e);
+ }
}
- if (newAuthZDN != null && newAuthZSet != null)
+ if (newAuthNDN != null && authNDN != null && authNDN.isSubordinateOrEqualTo(oldEntry.getName()))
{
- userMap.put(newAuthZDN, newAuthZSet);
+ if (newAuthNSet == null)
+ {
+ newAuthNSet = ConcurrentHashMap.newKeySet();
+ }
+ conn.getAuthenticationInfo().setAuthenticationDN(newAuthNDN);
+ newAuthNSet.add(conn);
+ }
+ if (newAuthZDN != null && authZDN != null && authZDN.isSubordinateOrEqualTo(oldEntry.getName()))
+ {
+ if (newAuthZSet == null)
+ {
+ newAuthZSet = ConcurrentHashMap.newKeySet();
+ }
+ conn.getAuthenticationInfo().setAuthorizationDN(newAuthZDN);
+ newAuthZSet.add(conn);
}
}
- }
- finally
- {
- lock.writeLock().unlock();
+ if (newAuthNDN != null && newAuthNSet != null)
+ {
+ userMap.put(newAuthNDN, newAuthNSet);
+ }
+ if (newAuthZDN != null && newAuthZSet != null)
+ {
+ userMap.put(newAuthZDN, newAuthZSet);
+ }
}
return PostResponse.continueOperationProcessing();
}
}
-
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/core/BindLatencyBenchmarkTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/core/BindLatencyBenchmarkTestCase.java
new file mode 100644
index 0000000..cb6ea2b
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/core/BindLatencyBenchmarkTestCase.java
@@ -0,0 +1,403 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions Copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.core;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.concurrent.TimeUnit;
+
+import org.opends.server.DirectoryServerTestCase;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.tools.RemoteConnection;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.*;
+
+/**
+ * Server side latency micro-benchmark for the simple BIND operation under high
+ * concurrency.
+ * <p>
+ * The benchmark opens {@code bind.bench.threads} persistent LDAP connections
+ * (one per thread), each repeatedly performing a simple BIND as a <em>random</em>
+ * one of {@code bind.bench.users} provisioned users for
+ * {@code bind.bench.durationSeconds} seconds, and reports the observed server
+ * side latency distribution (mean / p50 / p90 / p99 / max) and throughput.
+ * <p>
+ * Many connections binding as different users is the canonical high-concurrency
+ * authentication workload and is the scenario that stresses the per-bind
+ * bookkeeping done in {@code ClientConnection.setAuthenticationInfo()} /
+ * {@code AuthenticatedUsers}, whose user map is a concurrent map so that binds
+ * for different users do not serialize on a single global lock.
+ * <p>
+ * The benchmark is <strong>disabled by default</strong> so that it never runs as
+ * part of the normal test suite. Enable it explicitly, e.g.:
+ * <pre>
+ * JAVA_HOME=<jdk11> mvn -P precommit -pl opendj-server-legacy verify \
+ * -Dit.test=BindLatencyBenchmarkTestCase -DfailIfNoTests=false \
+ * -Dbind.bench=true -Dbind.bench.threads=200 \
+ * -Dbind.bench.durationSeconds=120 -Dbind.bench.label=before
+ * </pre>
+ * Results are printed to stdout and also written to
+ * {@code target/bind-bench-result-<label>.txt} (stdout may be suppressed
+ * during the test run).
+ */
+@SuppressWarnings("javadoc")
+public class BindLatencyBenchmarkTestCase extends DirectoryServerTestCase
+{
+ private static final String PASSWORD = "password";
+
+ /** Whether the benchmark is enabled (it is skipped otherwise). */
+ private static final boolean ENABLED = Boolean.getBoolean("bind.bench");
+ private static final int THREADS = Integer.getInteger("bind.bench.threads", 200);
+ /**
+ * Number of distinct users to provision and bind as. Each bind picks a random
+ * user, so binds spread across users (and, on the server, across the concurrent
+ * {@code AuthenticatedUsers} map) - this is what exposes per-bind lock
+ * contention. Defaults to the thread count.
+ */
+ private static final int USERS = Integer.getInteger("bind.bench.users", THREADS);
+
+ private static String userDN(int i)
+ {
+ return "uid=bench.user." + i + ",o=test";
+ }
+ private static final int DURATION_SECONDS = Integer.getInteger("bind.bench.durationSeconds", 120);
+ private static final int WARMUP_SECONDS = Integer.getInteger("bind.bench.warmupSeconds", 10);
+ private static final String LABEL = System.getProperty("bind.bench.label", "run");
+ private static final String HOST = System.getProperty("bind.bench.host", "127.0.0.1");
+
+ private volatile boolean running = true;
+ private volatile boolean recording;
+
+ @BeforeClass
+ public void setUp() throws Exception
+ {
+ TestCaseUtils.startServer();
+ TestCaseUtils.initializeTestBackend(true);
+ for (int i = 0; i < USERS; i++)
+ {
+ TestCaseUtils.addEntry(
+ "dn: " + userDN(i),
+ "objectClass: top",
+ "objectClass: person",
+ "objectClass: organizationalPerson",
+ "objectClass: inetOrgPerson",
+ "uid: bench.user." + i,
+ "givenName: Bench",
+ "sn: User " + i,
+ "cn: Bench User " + i,
+ "userPassword: " + PASSWORD);
+ }
+ }
+
+ @Test
+ public void benchmarkConcurrentBind() throws Exception
+ {
+ if (!ENABLED)
+ {
+ // Keep the regular test suite fast: the benchmark only runs when
+ // explicitly requested with -Dbind.bench=true.
+ System.out.println("BindLatencyBenchmarkTestCase skipped (set -Dbind.bench=true to run).");
+ return;
+ }
+
+ final int port = TestCaseUtils.getServerLdapPort();
+ final CountDownLatch ready = new CountDownLatch(THREADS);
+ final CountDownLatch startGate = new CountDownLatch(1);
+
+ final List<Worker> workers = new ArrayList<>(THREADS);
+ final List<Thread> threads = new ArrayList<>(THREADS);
+ for (int i = 0; i < THREADS; i++)
+ {
+ Worker w = new Worker(i, HOST, port, ready, startGate);
+ workers.add(w);
+ Thread t = new Thread(w, "bind-bench-" + i);
+ threads.add(t);
+ t.start();
+ }
+
+ // Wait until every worker has its connection ready, then release them all together.
+ assertTrue(ready.await(60, TimeUnit.SECONDS), "workers failed to connect in time");
+ startGate.countDown();
+
+ // Warm up (let JIT settle) without recording, then measure for the requested duration.
+ Thread.sleep(TimeUnit.SECONDS.toMillis(WARMUP_SECONDS));
+ long measureStart = System.nanoTime();
+ recording = true;
+ Thread.sleep(TimeUnit.SECONDS.toMillis(DURATION_SECONDS));
+ recording = false;
+ long measureEnd = System.nanoTime();
+ running = false;
+
+ for (Thread t : threads)
+ {
+ t.join(TimeUnit.SECONDS.toMillis(60));
+ }
+
+ // Aggregate results.
+ LatencyHistogram total = new LatencyHistogram();
+ long ops = 0;
+ long errors = 0;
+ for (Worker w : workers)
+ {
+ total.mergeFrom(w.hist);
+ ops += w.ops;
+ errors += w.errors;
+ }
+
+ double elapsedSeconds = (measureEnd - measureStart) / 1_000_000_000.0;
+ double throughput = ops / elapsedSeconds;
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n================ BIND latency benchmark [").append(LABEL).append("] ================\n");
+ sb.append(String.format(Locale.ROOT, "threads : %d%n", THREADS));
+ sb.append(String.format(Locale.ROOT, "measured duration : %.1f s (warmup %d s)%n", elapsedSeconds, WARMUP_SECONDS));
+ sb.append(String.format(Locale.ROOT, "bind operations : %d%n", ops));
+ sb.append(String.format(Locale.ROOT, "errors : %d%n", errors));
+ sb.append(String.format(Locale.ROOT, "throughput : %,.0f binds/s%n", throughput));
+ sb.append(String.format(Locale.ROOT, "latency mean : %.3f ms%n", total.meanMillis()));
+ sb.append(String.format(Locale.ROOT, "latency p50 : %.3f ms%n", total.percentileMillis(50.0)));
+ sb.append(String.format(Locale.ROOT, "latency p90 : %.3f ms%n", total.percentileMillis(90.0)));
+ sb.append(String.format(Locale.ROOT, "latency p99 : %.3f ms%n", total.percentileMillis(99.0)));
+ sb.append(String.format(Locale.ROOT, "latency p99.9 : %.3f ms%n", total.percentileMillis(99.9)));
+ sb.append(String.format(Locale.ROOT, "latency max : %.3f ms%n", total.maxMillis()));
+ sb.append("=========================================================================\n");
+ String report = sb.toString();
+
+ System.out.println(report);
+ writeReport(report);
+
+ // Basic sanity checks - this is a measurement, not a pass/fail gate.
+ assertEquals(errors, 0L, "some BIND operations failed");
+ assertTrue(ops > 0, "no BIND operations were recorded");
+ }
+
+ private void writeReport(String report)
+ {
+ String buildDir = System.getProperty("org.opends.server.BuildDir", "target");
+ File out = new File(buildDir, "bind-bench-result-" + LABEL + ".txt");
+ try (PrintWriter pw = new PrintWriter(out, "UTF-8"))
+ {
+ pw.print(report);
+ }
+ catch (Exception e)
+ {
+ System.out.println("Could not write benchmark report to " + out + ": " + e);
+ }
+ System.out.println("Benchmark report written to " + out.getAbsolutePath());
+ }
+
+ /** A single benchmark worker: owns one connection and binds in a tight loop. */
+ private final class Worker implements Runnable
+ {
+ private final int id;
+ private final String host;
+ private final int port;
+ private final CountDownLatch ready;
+ private final CountDownLatch startGate;
+ final LatencyHistogram hist = new LatencyHistogram();
+ long ops;
+ long errors;
+
+ Worker(int id, String host, int port, CountDownLatch ready, CountDownLatch startGate)
+ {
+ this.id = id;
+ this.host = host;
+ this.port = port;
+ this.ready = ready;
+ this.startGate = startGate;
+ }
+
+ @Override
+ public void run()
+ {
+ RemoteConnection conn = null;
+ try
+ {
+ conn = new RemoteConnection(host, port);
+ ready.countDown();
+ startGate.await();
+
+ while (running)
+ {
+ String dn = userDN(ThreadLocalRandom.current().nextInt(USERS));
+ long start = System.nanoTime();
+ try
+ {
+ conn.bind(dn, PASSWORD);
+ }
+ catch (Throwable t)
+ {
+ errors++;
+ conn = reconnect(conn);
+ continue;
+ }
+ long elapsed = System.nanoTime() - start;
+ if (recording)
+ {
+ hist.record(elapsed);
+ ops++;
+ }
+ }
+ }
+ catch (Throwable t)
+ {
+ errors++;
+ System.out.println("worker " + id + " aborted: " + t);
+ }
+ finally
+ {
+ close(conn);
+ }
+ }
+
+ private RemoteConnection reconnect(RemoteConnection old)
+ {
+ close(old);
+ try
+ {
+ return new RemoteConnection(host, port);
+ }
+ catch (Exception e)
+ {
+ return null;
+ }
+ }
+
+ private void close(RemoteConnection conn)
+ {
+ if (conn != null)
+ {
+ try
+ {
+ conn.close();
+ }
+ catch (Exception ignored)
+ {
+ // best effort
+ }
+ }
+ }
+ }
+
+ /**
+ * Compact log-linear latency histogram (HdrHistogram style, ~16 sub-buckets per
+ * power of two) with bounded memory and full dynamic range. Values are stored in
+ * microseconds; reporting is in milliseconds.
+ */
+ static final class LatencyHistogram
+ {
+ private static final int SUB_BITS = 4;
+ private static final int SUB_COUNT = 1 << SUB_BITS; // 16
+ private static final int SIZE = 512;
+
+ private final long[] counts = new long[SIZE];
+ private long count;
+ private long sumNanos;
+ private long maxNanos;
+
+ void record(long nanos)
+ {
+ long micros = (nanos + 500) / 1000;
+ counts[bucketIndex(micros)]++;
+ count++;
+ sumNanos += nanos;
+ if (nanos > maxNanos)
+ {
+ maxNanos = nanos;
+ }
+ }
+
+ void mergeFrom(LatencyHistogram other)
+ {
+ for (int i = 0; i < SIZE; i++)
+ {
+ counts[i] += other.counts[i];
+ }
+ count += other.count;
+ sumNanos += other.sumNanos;
+ if (other.maxNanos > maxNanos)
+ {
+ maxNanos = other.maxNanos;
+ }
+ }
+
+ static int bucketIndex(long micros)
+ {
+ if (micros < SUB_COUNT)
+ {
+ return (int) Math.max(0, micros);
+ }
+ int m = 63 - Long.numberOfLeadingZeros(micros); // floor(log2(micros))
+ int sub = (int) ((micros - (1L << m)) >> (m - SUB_BITS));
+ int idx = SUB_COUNT + (m - SUB_BITS) * SUB_COUNT + sub;
+ return Math.min(idx, SIZE - 1);
+ }
+
+ private static long bucketMidpointMicros(int idx)
+ {
+ if (idx < SUB_COUNT)
+ {
+ return idx;
+ }
+ int j = idx - SUB_COUNT;
+ int m = SUB_BITS + j / SUB_COUNT;
+ int sub = j % SUB_COUNT;
+ long lower = (1L << m) + ((long) sub << (m - SUB_BITS));
+ long width = 1L << (m - SUB_BITS);
+ return lower + width / 2;
+ }
+
+ double meanMillis()
+ {
+ return count == 0 ? 0.0 : (sumNanos / (double) count) / 1_000_000.0;
+ }
+
+ double maxMillis()
+ {
+ return maxNanos / 1_000_000.0;
+ }
+
+ double percentileMillis(double percentile)
+ {
+ if (count == 0)
+ {
+ return 0.0;
+ }
+ long target = (long) Math.ceil(percentile / 100.0 * count);
+ if (target < 1)
+ {
+ target = 1;
+ }
+ long cumulative = 0;
+ for (int i = 0; i < SIZE; i++)
+ {
+ cumulative += counts[i];
+ if (cumulative >= target)
+ {
+ return bucketMidpointMicros(i) / 1000.0;
+ }
+ }
+ return maxMillis();
+ }
+ }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/types/PrivilegeTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/types/PrivilegeTestCase.java
index 5c6b976..4429af2 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/types/PrivilegeTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/types/PrivilegeTestCase.java
@@ -27,7 +27,6 @@
import java.util.Map;
import java.util.Set;
import java.util.UUID;
-import java.util.concurrent.CopyOnWriteArraySet;
import org.forgerock.opendj.ldap.AttributeDescription;
import org.forgerock.opendj.ldap.ByteString;
@@ -2397,7 +2396,7 @@
{
conn.bind("cn=Test User,o=test", "password");
- CopyOnWriteArraySet<ClientConnection> connections = DirectoryServer
+ Set<ClientConnection> connections = DirectoryServer
.getAuthenticatedUsers().get(DN.valueOf("cn=Test User,o=test"));
assertNotNull(connections);
--
Gitblit v1.10.0