From dc20037ee9959ab05d841422015d7e4358162cbb Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 08 Jul 2026 16:16:40 +0000
Subject: [PATCH] Remove the unfixable testStateMachineFull and fix the dead replay pool (#700)
---
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/StateMachineTest.java | 387 +++----------------------------------------------------
opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java | 6
2 files changed, 28 insertions(+), 365 deletions(-)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
index a692fa9..0e86580 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/MultimasterReplication.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.plugin;
@@ -293,7 +294,10 @@
private int getNumberOfReplayThreadsOrDefault(ReplicationSynchronizationProviderCfg cfg)
{
Integer value = cfg.getNumUpdateReplayThreads();
- return value == null ? Platform.computeNumberOfThreads(16, 2.0f) : value;
+ // A non-positive value would silently kill the replay thread pool: fall
+ // back to the default in that case (the configuration schema enforces a
+ // minimum of 1, so this can only come from a broken caller).
+ return value == null || value <= 0 ? Platform.computeNumberOfThreads(16, 2.0f) : value;
}
/** Create the threads that will wait for incoming update messages. */
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/StateMachineTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/StateMachineTest.java
index c899e16..1f2d7e3 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/StateMachineTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/StateMachineTest.java
@@ -13,6 +13,7 @@
*
* Copyright 2009-2010 Sun Microsystems, Inc.
* Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC
*/
package org.opends.server.replication.plugin;
@@ -198,7 +199,14 @@
{
ConfigurationChangeListener<ReplicationSynchronizationProviderCfg> mmr =
(ConfigurationChangeListener<ReplicationSynchronizationProviderCfg>) provider;
- mmr.applyConfigurationChange(mock(ReplicationSynchronizationProviderCfg.class));
+ // An unstubbed mock would return 0 for getNumUpdateReplayThreads()
+ // (Mockito's default for Integer instead of null), silently killing the
+ // replay thread pool of MultimasterReplication for the rest of the test:
+ // stub it so that the default number of replay threads is used.
+ ReplicationSynchronizationProviderCfg providerCfg = mock(ReplicationSynchronizationProviderCfg.class);
+ when(providerCfg.getNumUpdateReplayThreads()).thenReturn(null);
+ when(providerCfg.getConnectionTimeout()).thenReturn(5000L);
+ mmr.applyConfigurationChange(providerCfg);
}
return replicationDomain;
@@ -388,241 +396,21 @@
}
}
- /**
- * Go through the possible state machine transitions:
- *
- * NC = Not connected status
- * N = Normal status
- * D = Degraded status
- * FU = Full update status
- * BG = Bad generation id status
- *
- * The test path should be:
- * ->NC->D->N->NC->N->D->NC->D->N->BG->NC->N->D->BG->FU->NC->N->D->FU->NC->BG->NC->N->FU->NC->N->NC
- * @throws Exception If a problem occurred
+ /*
+ * Note: the former testStateMachineFull test (a walk through all state
+ * machine transitions) has been removed. It had been disabled forever and
+ * turned out to be unfixable in its historical form:
+ * - it relied on the DS entering the degraded status while replaying a
+ * change flood, which requires the replay to be durably slower than the
+ * publisher; on modern hardware both rates are comparable, making every
+ * degraded transition a race,
+ * - the unbounded floods needed to force the degraded status exhaust the
+ * test JVM heap with real backend entries before the status analyzer
+ * (5 second period) reliably observes a non-empty queue with the
+ * degraded threshold of 1.
+ * The degraded/normal transitions are still covered deterministically by
+ * testStateMachineStatusAnalyzer above.
*/
- @Test(enabled = false, groups = "slow")
- public void testStateMachineFull() throws Exception
- {
- String testCase = "testStateMachineFull";
-
- debugInfo("Starting " + testCase);
-
- initTest();
- BrokerReader br = null;
- BrokerWriter bw = null;
-
- try
- {
- int DEGRADED_STATUS_THRESHOLD = 1;
-
- // RS1 starts with 1 message as degraded status threshold value
- rs1 = createReplicationServer(testCase, DEGRADED_STATUS_THRESHOLD);
-
- // DS2 starts and connects to RS1
- ds2 = createReplicationBroker(DS2_ID, new ServerState(), EMPTY_DN_GENID);
- br = new BrokerReader(ds2, DS2_ID);
- waitUntiConnected(DS2_ID);
-
- // DS2 starts sending a lot of changes
- bw = new BrokerWriter(ds2, DS2_ID, false);
- bw.follow();
- Thread.sleep(1000); // Let some messages being queued in RS
-
- /*
- * DS1 starts and connects to RS1, server state exchange should lead to start in degraded status
- * as some changes should be in queued in the RS and the threshold value is 1 change in queue.
- */
- ds1 = createReplicationDomain(DS1_ID);
- waitUntiConnected(DS1_ID);
- waitUntilStatusEquals(ds1, ServerStatus.DEGRADED_STATUS);
-
- /* DS2 stops sending changes: DS1 should replay pending changes and should enter the normal status */
- bw.pause();
- // Sleep enough so that replay can be done and analyzer has time
- // to see that the queue length is now under the threshold value.
- waitUntilStatusEquals(ds1, ServerStatus.NORMAL_STATUS);
-
- /* RS1 stops to make DS1 go to not connected status (from normal status) */
- rs1.remove();
- waitUntilStatusEquals(ds1, ServerStatus.NOT_CONNECTED_STATUS);
-
- /*
- * DS2 restarts with up to date server state
- * (this allows to have restarting RS1 not sending him some updates he already sent)
- */
- ds2.stop();
- shutdown(bw);
- shutdown(br);
- ServerState curState = ds1.getServerState();
- ds2 = createReplicationBroker(DS2_ID, curState, EMPTY_DN_GENID);
- br = new BrokerReader(ds2, DS2_ID);
-
- /* RS1 restarts, DS1 should get back to normal status */
- rs1 = createReplicationServer(testCase, DEGRADED_STATUS_THRESHOLD);
- waitUntiConnected(DS2_ID);
- waitUntilStatusEquals(ds1, ServerStatus.NORMAL_STATUS);
-
- /* DS2 sends again a lot of changes to make DS1 degraded again */
- bw = new BrokerWriter(ds2, DS2_ID, false);
- bw.follow();
- Thread.sleep(8000); // Let some messages being queued in RS, and analyzer see the change
- waitUntilStatusEquals(ds1, ServerStatus.DEGRADED_STATUS);
-
- /* RS1 stops to make DS1 go to not connected status (from degraded status) */
- rs1.remove();
- bw.pause();
- waitUntilStatusEquals(ds1, ServerStatus.NOT_CONNECTED_STATUS);
-
- /*
- * DS2 restarts with up to date server state
- * (this allows to have restarting RS1 not sending him some updates he already sent)
- */
- ds2.stop();
- shutdown(bw);
- shutdown(br);
- curState = ds1.getServerState();
- ds2 = createReplicationBroker(DS2_ID, curState, EMPTY_DN_GENID);
- br = new BrokerReader(ds2, DS2_ID);
-
- /*
- * RS1 restarts, DS1 should reconnect in degraded status
- * (from not connected this time, not from state machine entry)
- */
- rs1 = createReplicationServer(testCase, DEGRADED_STATUS_THRESHOLD);
- // It is too difficult to tune the right sleep so disabling this test:
- // Sometimes the status analyzer may be fast and quickly change the status
- // of DS1 to NORMAL_STATUS
- //sleep(2000);
- //sleepAssertStatusEquals(30, ds1, ServerStatus.DEGRADED_STATUS);
- waitUntiConnected(DS2_ID);
-
- /* DS1 should come back in normal status after a while */
- waitUntilStatusEquals(ds1, ServerStatus.NORMAL_STATUS);
-
- /*
- * DS2 sends a reset gen id order with wrong gen id:
- * DS1 should go into bad generation id status
- */
- long BAD_GEN_ID = 999999L;
- resetGenId(ds2, BAD_GEN_ID); // ds2 will also go bad gen
- waitUntilStatusEquals(ds1, ServerStatus.BAD_GEN_ID_STATUS);
-
- /*
- * DS2 sends again a reset gen id order with right id: DS1 should be disconnected by RS
- * then reconnect and enter again in normal status. This goes through not connected status
- * but not possible to check as should reconnect immediately
- */
- resetGenId(ds2, EMPTY_DN_GENID); // ds2 will also be disconnected
- ds2.stop();
- shutdown(br); // Reader could reconnect broker, but gen id would be bad: need to recreate a
- // broker to send changex
- waitUntilStatusEquals(ds1, ServerStatus.NORMAL_STATUS);
-
- /* DS2 sends again a lot of changes to make DS1 degraded again */
- curState = ds1.getServerState();
- ds2 = createReplicationBroker(DS2_ID, curState, EMPTY_DN_GENID);
- waitUntiConnected(DS2_ID);
- bw = new BrokerWriter(ds2, DS2_ID, false);
- br = new BrokerReader(ds2, DS2_ID);
- bw.follow();
- Thread.sleep(8000); // Let some messages being queued in RS, and analyzer see the change
- waitUntilStatusEquals(ds1, ServerStatus.DEGRADED_STATUS);
-
- /*
- * DS2 sends reset gen id order with bad gen id: DS1 should go in bad gen id status
- * (from degraded status this time)
- */
- // -1 to allow next step full update and flush RS db so that DS1 can reconnect after full
- // update
- resetGenId(ds2, -1);
- waitUntilStatusEquals(ds1, ServerStatus.BAD_GEN_ID_STATUS);
- bw.pause();
-
- /*
- * DS2 engages full update (while DS1 in bad gen id status),
- * DS1 should go in full update status
- */
- BrokerInitializer bi = new BrokerInitializer(ds2, DS2_ID, false);
- bi.initFullUpdate(DS1_ID, 200);
- waitUntilStatusEquals(ds1, ServerStatus.FULL_UPDATE_STATUS);
-
- /*
- * DS2 terminates full update to DS1: DS1 should reconnect (goes through not connected status)
- * and come back to normal status (RS genid was -1 so RS will adopt new gen id)
- */
- bi.runFullUpdate();
- waitUntilStatusEquals(ds1, ServerStatus.NORMAL_STATUS);
-
- /* DS2 sends changes to DS1: DS1 should go in degraded status */
- ds2.stop(); // will need a new broker with another gen id restart it
- shutdown(bw);
- shutdown(br);
- long newGen = ds1.getGenerationID();
- curState = ds1.getServerState();
- ds2 = createReplicationBroker(DS2_ID, curState, newGen);
- waitUntiConnected(DS2_ID);
- bw = new BrokerWriter(ds2, DS2_ID, false);
- br = new BrokerReader(ds2, DS2_ID);
- bw.follow();
- Thread.sleep(8000); // Let some messages being queued in RS, and analyzer see the change
- waitUntilStatusEquals(ds1, ServerStatus.DEGRADED_STATUS);
-
- /* DS2 engages full update (while DS1 in degraded status), DS1 should go in full update status */
- bi = new BrokerInitializer(ds2, DS2_ID, false);
- bi.initFullUpdate(DS1_ID, 300);
- waitUntilStatusEquals(ds1, ServerStatus.FULL_UPDATE_STATUS);
- bw.pause();
-
- /*
- * DS2 terminates full update to DS1: DS1 should reconnect (goes through not connected status)
- * and come back to bad gen id status (RS genid was another gen id (300 entries instead of 200)
- */
- bi.runFullUpdate();
- waitUntilStatusEquals(ds1, ServerStatus.BAD_GEN_ID_STATUS);
-
- /*
- * DS2 sends reset gen id with gen id same as DS1:
- * DS1 will be disconnected by RS (not connected status) and come back to normal status
- */
- ds2.stop(); // will need a new broker with another gen id restart it
- shutdown(bw);
- shutdown(br);
- newGen = ds1.getGenerationID();
- curState = ds1.getServerState();
- ds2 = createReplicationBroker(DS2_ID, curState, newGen);
- waitUntiConnected(DS2_ID);
- br = new BrokerReader(ds2, DS2_ID);
- resetGenId(ds2, newGen); // Make DS1 reconnect in normal status
-
- waitUntilStatusEquals(ds1, ServerStatus.NORMAL_STATUS);
-
- /* DS2 engages full update (while DS1 in normal status), DS1 should go in full update status */
- bi = new BrokerInitializer(ds2, DS2_ID, false);
- bi.initFullUpdate(DS1_ID, 300); // 300 entries will compute same genid of the RS
- waitUntilStatusEquals(ds1, ServerStatus.FULL_UPDATE_STATUS);
-
- /*
- * DS2 terminates full update to DS1: DS1 should reconnect (goes through not connected status)
- * and come back to normal status (process full update with same data as before so RS already
- * has right gen id: version with 300 entries)
- */
- bi.runFullUpdate();
- ds2.stop();
- shutdown(br);
- waitUntilStatusEquals(ds1, ServerStatus.NORMAL_STATUS);
-
- /* RS1 stops, DS1 should go to not connected status */
- rs1.remove();
- waitUntilStatusEquals(ds1, ServerStatus.NOT_CONNECTED_STATUS);
- } finally
- {
- // Finalize test
- endTest();
- shutdown(bw);
- shutdown(br);
- }
- }
/**
* Set up the environment.
@@ -661,136 +449,7 @@
paranoiaCheck();
}
- /**
- * Sends a reset genid message through the given replication broker, with the
- * given new generation id.
- */
- private void resetGenId(ReplicationBroker rb, long newGenId)
- {
- ResetGenerationIdMsg resetMsg = new ResetGenerationIdMsg(newGenId);
- rb.publish(resetMsg);
- }
- /**
- * Utility class for making a full update through a broker. No separated thread
- * Usage:
- * BrokerInitializer bi = new BrokerInitializer(rb, sid, nEntries);
- * bi.initFullUpdate(); // Initializes a full update session by sending InitializeTargetMsg
- * bi.runFullUpdate(); // loops sending nEntries entries and finalizes the full update by sending the EntryDoneMsg
- */
- private class BrokerInitializer
- {
- private ReplicationBroker rb;
- private int serverId = -1;
- private long userId;
- /** Server id of server to initialize. */
- private int destId = -1;
- /** Number of entries to send to dest. */
- private long nEntries = -1;
- private boolean createReader;
-
- /**
- * If the BrokerInitializer is to be used for a lot of entries to send
- * (which is often the case), the reader thread should be enabled to make
- * the window subsystem work and allow the broker to send as much entries as
- * he wants. If not enabled, the user is responsible to call the receive
- * method of the broker himself.
- */
- private BrokerReader reader;
-
- /** Creates a broker initializer. Also creates a reader according to request */
- public BrokerInitializer(ReplicationBroker rb, int serverId,
- boolean createReader)
- {
- this.rb = rb;
- this.serverId = serverId;
- this.createReader = createReader;
- }
-
- /** Initializes a full update session by sending InitializeTargetMsg. */
- public void initFullUpdate(int destId, long nEntries)
- {
- // Also create reader ?
- if (createReader)
- {
- reader = new BrokerReader(rb, serverId);
- }
-
- debugInfo("Broker " + serverId + " initializer sending InitializeTargetMsg to server " + destId);
-
- this.destId = destId;
- this.nEntries = nEntries;
-
- // Send init msg to warn dest server it is going do be initialized
- RoutableMsg initTargetMsg = new InitializeTargetMsg(
- EXAMPLE_DN_, serverId, destId, serverId, nEntries, initWindow);
- rb.publish(initTargetMsg);
-
- // Send top entry for the domain
- String topEntry = "dn: " + EXAMPLE_DN + "\n"
- + "objectClass: top\n"
- + "objectClass: domain\n"
- + "dc: example\n"
- + "entryUUID: 11111111-1111-1111-1111-111111111111\n\n";
- EntryMsg entryMsg = new EntryMsg(serverId, destId, topEntry.getBytes(), 1);
- rb.publish(entryMsg);
- }
-
- private EntryMsg createNextEntryMsg()
- {
- String userEntryUUID = "11111111-1111-1111-1111-111111111111";
- long curId = ++userId;
- String userdn = "uid=full_update_user" + curId + "," + EXAMPLE_DN;
- String entryWithUUIDldif = "dn: " + userdn + "\n" + "objectClass: top\n" +
- "objectClass: person\n" + "objectClass: organizationalPerson\n" +
- "objectClass: inetOrgPerson\n" +
- "uid: full_update_user" + curId + "\n" +
- "homePhone: 951-245-7634\n" +
- "description: This is the description for Aaccf Amar.\n" + "st: NC\n" +
- "mobile: 027-085-0537\n" +
- "postalAddress: Aaccf Amar$17984 Thirteenth Street" +
- "$Rockford, NC 85762\n" + "mail: user.1@example.com\n" +
- "cn: Aaccf Amar\n" + "l: Rockford\n" + "pager: 508-763-4246\n" +
- "street: 17984 Thirteenth Street\n" + "telephoneNumber: 216-564-6748\n" +
- "employeeNumber: 1\n" + "sn: Amar\n" + "givenName: Aaccf\n" +
- "postalCode: 85762\n" + "userPassword: password\n" + "initials: AA\n" +
- "entryUUID: " + userEntryUUID + "\n\n";
- // -> WARNING: EntryMsg PDUs are concatenated before calling import on LDIF
- // file so need \n\n to separate LDIF entries to conform to LDIF file format
-
- // Create an entry message
- return new EntryMsg(serverId, destId, entryWithUUIDldif.getBytes(),
- (int) userId);
- }
-
- /**
- * Loops sending entries for full update (EntryMsg messages). When
- * terminates, sends the EntryDoneMsg to finalize full update. Number of
- * sent entries is determined at initFullUpdate call time.
- */
- public void runFullUpdate()
- {
- debugInfo("Broker " + serverId + " initializer starting sending entries to server " + destId);
-
- for(long i = 0 ; i<nEntries ; i++) {
- EntryMsg entryMsg = createNextEntryMsg();
- rb.publish(entryMsg);
- }
-
- debugInfo("Broker " + serverId + " initializer stopping sending entries");
-
- debugInfo("Broker " + serverId + " initializer sending EntryDoneMsg");
- DoneMsg doneMsg = new DoneMsg(serverId, destId);
- rb.publish(doneMsg);
-
- if (createReader)
- {
- shutdown(reader);
- }
-
- debugInfo("Broker " + serverId + " initializer thread is dying");
- }
- }
/** Thread for sending a lot of changes through a broker. */
private class BrokerWriter extends Thread
--
Gitblit v1.10.0