mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

7 files modified
1 files added
334 ■■■■■ changed files
.github/workflows/benchmark.yml 6 ●●●●● patch | view | raw | blame | history
.github/workflows/build.yml 30 ●●●●● patch | view | raw | blame | history
.github/workflows/codeql.yml 4 ●●●●● patch | view | raw | blame | history
.github/workflows/deploy.yml 24 ●●●●● patch | view | raw | blame | history
.github/workflows/docker-scan.yml 2 ●●●●● patch | view | raw | blame | history
.github/workflows/release.yml 4 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationDomainMonitor.java 18 ●●●●● patch | view | raw | blame | history
opendj-server-legacy/src/test/java/org/opends/server/replication/server/ReplicationDomainMonitorTest.java 246 ●●●●● patch | view | raw | blame | history
.github/workflows/benchmark.yml
@@ -278,7 +278,7 @@
      - name: Upload JMeter reports
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: jmeter-reports
          path: |
@@ -290,7 +290,7 @@
      - name: Upload OpenLDAP logs
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: logs-openldap
          path: logs/openldap/
@@ -299,7 +299,7 @@
      - name: Upload OpenDJ logs
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: logs-opendj
          path: logs/opendj/
.github/workflows/build.yml
@@ -98,7 +98,7 @@
    # own, so a byte-for-byte gate would fire without a source change.
    - name: Upload Windows exe artifacts
      if: runner.os == 'Windows'
      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
      uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
      with:
        name: windows-exe-${{ matrix.java }}
        retention-days: 5
@@ -451,7 +451,7 @@
        done
    - name: Upload artifacts OpenDJ Server
      uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
      uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
      with:
        name: ${{ matrix.os }}-${{ matrix.java }}
        retention-days: 5
@@ -485,7 +485,7 @@
            .github/benchmark
            .github/scripts
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: ubuntu-latest-11
      - name: Get latest release version
@@ -905,7 +905,7 @@
          scanners: vuln
          cache: false
      - name: Upload Trivy report to GitHub Security
        uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
        uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
        # upload even if a preceding step failed, but not without a report to upload
        if: ${{ always() && hashFiles('trivy-results.sarif') != '' }}
        with:
@@ -925,7 +925,7 @@
            "PDB" localhost:5000/${GITHUB_REPOSITORY,,}:${{ env.release_version }}
      - name: Upload benchmark artifacts (PDB vs JE)
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: benchmark-pdb-vs-je
          path: |
@@ -944,7 +944,7 @@
            "Release" openidentityplatform/opendj:latest
      - name: Upload benchmark artifacts
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: benchmark-build-vs-release
          path: |
@@ -975,7 +975,7 @@
            .github/benchmark
            .github/scripts
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: ubuntu-latest-11
      - name: Get latest release version
@@ -1396,7 +1396,7 @@
          scanners: vuln
          cache: false
      - name: Upload Trivy report to GitHub Security
        uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
        uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
        # upload even if a preceding step failed, but not without a report to upload
        if: ${{ always() && hashFiles('trivy-results.sarif') != '' }}
        with:
@@ -1416,7 +1416,7 @@
            "Release-alpine" openidentityplatform/opendj:alpine
      - name: Upload benchmark artifacts
        if: always()
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: benchmark-build-vs-release-alpine
          path: |
@@ -1434,7 +1434,7 @@
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: ubuntu-latest-11
      - name: Clean-room install + SysV start/stop (debian:12 container)
@@ -1516,7 +1516,7 @@
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: ubuntu-latest-11
      - name: Install and start/stop in Rocky Linux 9
@@ -1567,7 +1567,7 @@
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: ubuntu-latest-11
      - name: Download released 5.1.2 deb
@@ -1617,7 +1617,7 @@
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: ubuntu-latest-11
      - name: Download released 5.1.2 rpm
@@ -1755,7 +1755,7 @@
            .github/scripts
            opendj-packages/opendj-msi/opendj-msi-standard/resources/msi
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: windows-latest-11
      - name: Set up Java
@@ -1943,7 +1943,7 @@
      contents: read
    steps:
      - name: Download artifacts
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: windows-latest-11
      - name: Set up Java
.github/workflows/codeql.yml
@@ -85,7 +85,7 @@
          submodules: recursive
      - name: Initialize CodeQL
        uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
        uses: github/codeql-action/init@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
        with:
          languages: ${{ matrix.language }}
          build-mode: ${{ matrix.build-mode }}
@@ -139,6 +139,6 @@
      # ---------------------------------------------------------------------
      - name: Perform CodeQL Analysis
        uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
        uses: github/codeql-action/analyze@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
        with:
          category: "/language:${{ matrix.language }}"
.github/workflows/deploy.yml
@@ -89,7 +89,7 @@
      # this cannot loop; a PAT would break that.
      - name: Download the launchers built by the triggering Build run
        continue-on-error: true
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: windows-exe-11
          run-id: ${{ github.event.workflow_run.id }}
@@ -227,22 +227,22 @@
        continue-on-error: true
        run: mvn javadoc:aggregate
      - name: Upload artifacts OpenDJ Server
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ Server
         path: opendj-server-legacy/target/package/*.zip
      - name: Upload artifacts OpenDJ SDK Toolkit
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ SDK Toolkit
         path: opendj-ldap-toolkit/target/*.zip
      - name: Upload artifacts OpenDJ Debian Package
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ Debian Package
         path: opendj-packages/opendj-deb/opendj-deb-standard/target/*.deb
      - name: Upload artifacts OpenDJ RPM Package
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ RPM Package
         path: opendj-packages/opendj-rpm/opendj-rpm-standard/target/rpm/opendj/RPMS/noarch/*.rpm
@@ -250,7 +250,7 @@
      # Build run (windows-latest-11 artifact) instead of rebuilding it here.
      - name: Download Windows build artifact (contains the MSI)
        continue-on-error: true
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: windows-latest-11
          run-id: ${{ github.event.workflow_run.id }}
@@ -258,7 +258,7 @@
          path: windows-build
      - name: Upload artifacts OpenDJ MSI Package
        continue-on-error: true
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ MSI Package
         path: windows-build/opendj-packages/opendj-msi/opendj-msi-standard/target/*.msi
@@ -266,27 +266,27 @@
         # continue-on-error) instead of warning and publishing nothing.
         if-no-files-found: error
      - name: Upload artifacts OpenDJ Docker Packages
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ Docker Packages
         path: opendj-packages/opendj-docker/target/Dockerfile.zip
      - name: Upload artifacts OpenDJ Openshift template
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ Openshift template
         path: opendj-packages/opendj-openshift-template/*.yaml
      - name: Upload artifacts OpenDJ Doc Generated Reference
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ Doc Generated References
         path: opendj-doc-generated-ref/target/*.zip
      - name: Upload artifacts OpenDJ DSML Gateway
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ DSML Gateway
         path: opendj-dsml-servlet/target/*.war
      - name: Upload artifacts OpenDJ Commons REST LDAP Gateway
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
         name: OpenDJ Commons REST LDAP Gateway
         path: opendj-rest2ldap-servlet/target/*.war
.github/workflows/docker-scan.yml
@@ -51,7 +51,7 @@
          scanners: vuln
          cache: false
      - name: Upload report to GitHub Security
        uses: github/codeql-action/upload-sarif@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
        uses: github/codeql-action/upload-sarif@24c54180a607b1449ed407dd24f251e4e9147c8d # v4.38.3
        # upload even if a preceding step failed, but not without a report to upload
        if: ${{ always() && hashFiles(format('trivy-{0}.sarif', matrix.tag)) != '' }}
        with:
.github/workflows/release.yml
@@ -104,7 +104,7 @@
      # built on Windows), so it does not have to rebuild opendj-server-legacy.
      - name: Upload the server zip for the MSI job
        continue-on-error: true
        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
        uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
        with:
          name: release-server-zip
          retention-days: 1
@@ -215,7 +215,7 @@
          wix --version
          wix extension add -g WixToolset.UI.wixext/5.0.2 || true
      - name: Download the server zip built by release-maven
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
        uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
        with:
          name: release-server-zip
          path: server-zip
opendj-server-legacy/src/main/java/org/opends/server/replication/server/ReplicationDomainMonitor.java
@@ -13,6 +13,7 @@
 *
 * Copyright 2006-2010 Sun Microsystems, Inc.
 * Portions Copyright 2011-2016 ForgeRock AS.
 * Portions Copyright 2026 3A Systems, LLC.
 */
package org.opends.server.replication.server;
@@ -201,6 +202,8 @@
            pendingMonitorData.completeComputing();
            monitorData = pendingMonitorData;
            monitorDataLastBuildDate = TimeThread.getTime();
            // A late answer must not go into the published data: it would come after our own state.
            pendingMonitorData = null;
          }
        }
        finally
@@ -219,7 +222,17 @@
    return monitorData;
  }
  /** Start collecting global monitoring information for the replication domain. */
  /**
   * Takes the states of this RS and of its directly connected DSs into the pending monitor data:
   * once when the computing starts, and again after each answer of another RS is stored.
   * <p>
   * The changes of our directly connected DSs reach the other RSs through us only, so a state of
   * ours taken before theirs would show us behind them on those changes, and a DS reading that in
   * the monitoring message would leave us for an RS which only has its changes from us (issue
   * #1198). Taken again at each answer rather than once the wait for the answers is over, our
   * state is no later than the last answer either: an RS which does not answer would otherwise put
   * the whole wait between our state and the answers of the others, and they would look behind.
   */
  private void initializePendingMonitorData()
  {
    // Let's process our directly connected DS
@@ -318,6 +331,9 @@
            }
          }
        }
        // Our own state must not be older than the one this RS just answered with (issue #1198)
        initializePendingMonitorData();
      }
      catch (RuntimeException e)
      {
opendj-server-legacy/src/test/java/org/opends/server/replication/server/ReplicationDomainMonitorTest.java
New file
@@ -0,0 +1,246 @@
/*
 * 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.replication.server;
import static java.util.concurrent.TimeUnit.*;
import static org.mockito.Matchers.*;
import static org.mockito.Mockito.*;
import static org.testng.Assert.*;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.opendj.ldap.DN;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.protocol.MonitorMsg;
import org.opends.server.replication.protocol.ReplicationMsg;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
/**
 * Tests the states {@link ReplicationDomainMonitor} gathers for the replication servers of a
 * domain, which the monitoring messages hand to the directory servers.
 * <p>
 * A directory server connected to a replication server checks on those messages whether its
 * replication server still holds its newest change, and leaves it when another one holds it and
 * it does not (issue #1198). The changes of a directory server reach the other replication
 * servers through the one it is connected to only, so none of them can hold one of its changes
 * which that one does not - unless the states compared were not taken at the same moment.
 */
@SuppressWarnings("javadoc")
public class ReplicationDomainMonitorTest extends DirectoryServerTestCase
{
  private static final DN BASE_DN = DN.valueOf("dc=example,dc=com");
  private static final int LOCAL_RS_ID = 1;
  private static final int PEER_RS_ID = 2;
  private static final int SILENT_RS_ID = 3;
  private static final int DS_ID = 10;
  private static final CSN FIRST = new CSN(1000, 1, DS_ID);
  private static final CSN FORWARDED = new CSN(1000, 2, DS_ID);
  private static final CSN AHEAD = new CSN(1000, 5, DS_ID);
  private static final long STEP_TIMEOUT_MS = 10000;
  /** What the reader of the session to a peer does once the monitor request has been sent to it. */
  private interface PeerReader
  {
    void run(ReplicationDomainMonitor monitor, Thread requester) throws Exception;
  }
  /** The state of the changelog of this replication server, which the mocked domain reports. */
  private AtomicReference<ServerState> changelog;
  /** Counted down once the monitor data is published. */
  private CountDownLatch published;
  private Map<Integer, Thread> readers;
  private AtomicReference<Throwable> readerFailure;
  @BeforeMethod
  public void setUp()
  {
    changelog = new AtomicReference<>(stateWith(FIRST));
    published = new CountDownLatch(1);
    readers = new ConcurrentHashMap<>();
    readerFailure = new AtomicReference<>();
  }
  /**
   * A change of a directory server connected to this replication server comes in while the
   * answers are waited for, and this replication server forwards it to its peer before the peer
   * answers: the state reported for this replication server must not be older than the one of its
   * peer on the changes of that directory server.
   */
  @Test
  public void aPeerDoesNotLookAheadOnTheChangesItGetsFromThisReplicationServer() throws Exception
  {
    final Map<Integer, PeerReader> peers = new LinkedHashMap<>();
    peers.put(PEER_RS_ID, (monitor, requester) ->
    {
      awaitTheWaitForTheAnswers(requester);
      changelog.set(stateWith(FORWARDED));
      monitor.receiveMonitorDataResponse(answer(PEER_RS_ID, FORWARDED), PEER_RS_ID);
    });
    final ReplicationDomainMonitorData data = recompute(peers);
    final CSN local = data.getRSStates(LOCAL_RS_ID).getCSN(DS_ID);
    final CSN peer = stateOf(data, PEER_RS_ID).getCSN(DS_ID);
    assertFalse(local.isOlderThan(peer), "RS(" + LOCAL_RS_ID + ") is reported at " + local.toStringUI()
        + ", behind RS(" + PEER_RS_ID + ") at " + peer.toStringUI()
        + ", which got the changes of DS(" + DS_ID + ") from it");
  }
  /**
   * A peer can hold changes of a directory server which this replication server does not, when
   * the directory server published them to that peer on an earlier session: the peer must still
   * be reported ahead then.
   */
  @Test
  public void aPeerWhichHoldsChangesThisReplicationServerLacksIsReportedAhead() throws Exception
  {
    final Map<Integer, PeerReader> peers = new LinkedHashMap<>();
    peers.put(PEER_RS_ID,
        (monitor, requester) -> monitor.receiveMonitorDataResponse(answer(PEER_RS_ID, AHEAD), PEER_RS_ID));
    final ReplicationDomainMonitorData data = recompute(peers);
    assertEquals(data.getRSStates(LOCAL_RS_ID).getCSN(DS_ID), FIRST);
    assertEquals(stateOf(data, PEER_RS_ID).getCSN(DS_ID), AHEAD);
  }
  /**
   * One peer answers at once, another one only once the wait for the answers is over, and changes
   * come in meanwhile: the peer which answered is not shown missing the changes which came in
   * while the other one was waited for, and the late answer does not go into the published data.
   */
  @Test
  public void aPeerWhichAnswersAfterTheWaitLeavesTheOthersUpToDate() throws Exception
  {
    final Map<Integer, PeerReader> peers = new LinkedHashMap<>();
    peers.put(PEER_RS_ID,
        (monitor, requester) -> monitor.receiveMonitorDataResponse(answer(PEER_RS_ID, FIRST), PEER_RS_ID));
    peers.put(SILENT_RS_ID, (monitor, requester) ->
    {
      awaitTheWaitForTheAnswers(requester);
      readers.get(PEER_RS_ID).join(STEP_TIMEOUT_MS);
      changelog.set(stateWith(FORWARDED));
      assertTrue(published.await(STEP_TIMEOUT_MS, MILLISECONDS), "the monitor data was not published");
      monitor.receiveMonitorDataResponse(answer(SILENT_RS_ID, FORWARDED), SILENT_RS_ID);
    });
    final ReplicationDomainMonitorData data = recompute(peers);
    assertEquals(data.getMissingChangesRS(PEER_RS_ID), 0, "RS(" + PEER_RS_ID + ") answered with every "
        + "change there was then, and is shown missing the ones which came in while RS(" + SILENT_RS_ID
        + ") was waited for");
    assertNull(data.getRSStates(SILENT_RS_ID),
        "the answer of RS(" + SILENT_RS_ID + "), which came after the wait, went into the published data");
  }
  /**
   * Gathers the monitor data of a domain with the provided peers, in this order: the reader of
   * the session to each of them runs once the monitor request is sent to it. The readers are done
   * when this returns.
   */
  private ReplicationDomainMonitorData recompute(Map<Integer, PeerReader> peerReaders) throws Exception
  {
    final ReplicationServerDomain domain = mock(ReplicationServerDomain.class);
    when(domain.getBaseDN()).thenReturn(BASE_DN);
    when(domain.getLocalRSServerId()).thenReturn(LOCAL_RS_ID);
    when(domain.getConnectedDSs()).thenReturn(Collections.<Integer, DataServerHandler> emptyMap());
    when(domain.getLatestServerState()).thenAnswer(invocation -> changelog.get().duplicate());
    final ReplicationDomainMonitor monitor = new ReplicationDomainMonitor(domain);
    final Map<Integer, ReplicationServerHandler> peers = new LinkedHashMap<>();
    for (Map.Entry<Integer, PeerReader> entry : peerReaders.entrySet())
    {
      final int peerId = entry.getKey();
      final PeerReader peerReader = entry.getValue();
      final ReplicationServerHandler peer = mock(ReplicationServerHandler.class);
      when(peer.getServerId()).thenReturn(peerId);
      doAnswer(invocation ->
      {
        final Thread requester = Thread.currentThread();
        final Thread reader = new Thread(() ->
        {
          try
          {
            peerReader.run(monitor, requester);
          }
          catch (Throwable e)
          {
            readerFailure.compareAndSet(null, e);
          }
        }, "reader of the session to RS(" + peerId + ")");
        reader.setDaemon(true);
        readers.put(peerId, reader);
        reader.start();
        return null;
      }).when(peer).send(any(ReplicationMsg.class));
      peers.put(peerId, peer);
    }
    when(domain.getConnectedRSs()).thenReturn(peers);
    final ReplicationDomainMonitorData data = monitor.recomputeMonitorData();
    published.countDown();
    for (Map.Entry<Integer, Thread> reader : readers.entrySet())
    {
      reader.getValue().join(STEP_TIMEOUT_MS);
      assertFalse(reader.getValue().isAlive(), "the reader of the session to RS(" + reader.getKey() + ") is stuck");
    }
    if (readerFailure.get() != null)
    {
      throw new AssertionError("a reader failed", readerFailure.get());
    }
    return data;
  }
  /** Returns once the requests are all sent, and the requester waits for the answers. */
  private static void awaitTheWaitForTheAnswers(Thread requester) throws InterruptedException
  {
    final long deadline = System.currentTimeMillis() + STEP_TIMEOUT_MS;
    while (requester.getState() != Thread.State.TIMED_WAITING)
    {
      assertTrue(System.currentTimeMillis() < deadline, "the requester did not wait for the answers");
      Thread.sleep(1);
    }
  }
  private static ServerState stateOf(ReplicationDomainMonitorData data, int rsId)
  {
    final ServerState state = data.getRSStates(rsId);
    assertNotNull(state, "the answer of RS(" + rsId + ") did not come within the wait");
    return state;
  }
  private static MonitorMsg answer(int rsId, CSN newest)
  {
    final MonitorMsg answer = new MonitorMsg(rsId, LOCAL_RS_ID);
    answer.setReplServerDbState(stateWith(newest));
    return answer;
  }
  private static ServerState stateWith(CSN csn)
  {
    final ServerState state = new ServerState();
    state.update(csn);
    return state;
  }
}