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=&lt;jdk11&gt; 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-&lt;label&gt;.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