From 92d88ca699cd8090a26b92cbe46789d2b848195f Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 07 Sep 2026 09:30:36 +0000
Subject: [PATCH] [#889] Keep a change the replay could not apply out of the ServerState (#892)

---
 opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChange.java |   84 +++++++++++++++++++++++++++++++++++++++++
 1 files changed, 83 insertions(+), 1 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChange.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChange.java
index 38f0661..34f4c4f 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChange.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/plugin/PendingChange.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2008 Sun Microsystems, Inc.
  * Portions copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.replication.plugin;
 
@@ -29,7 +30,29 @@
 {
   private final CSN csn;
   private boolean committed;
-  private UpdateMsg msg;
+  /**
+   * Written when the delivery which owns a remote change is taken over by the one which
+   * follows it, and read by the dependency checks without the pending changes lock.
+   */
+  private volatile UpdateMsg msg;
+  /**
+   * Whether a replay thread owns this change: it is being replayed, or it waits for the
+   * change it depends on. A remote change which no thread owns is one whose replay
+   * failed and which the replication server is expected to deliver again.
+   */
+  private boolean owned;
+  /**
+   * How many times in a row the replay of this change failed, and when the first of
+   * those failures happened - on a clock which only moves forward.
+   * <p>
+   * They live here, on the change which stays listed as the barrier holding the
+   * ServerState back, rather than in a map on the side: a bound on such a map would have
+   * a change evicted between two of its own failures and its give-up budget restarted,
+   * so a replica failing more changes than the bound would never give up on any of them
+   * (issue #889).
+   */
+  private int replayFailures;
+  private long firstReplayFailureTimeMs;
   private final PluginOperation op;
 
   /**
@@ -108,6 +131,65 @@
   }
 
   /**
+   * Returns whether a replay thread owns this change.
+   *
+   * @return {@code true} if a replay thread is replaying this change or waiting for the
+   *         change it depends on
+   */
+  public boolean isOwned()
+  {
+    return owned;
+  }
+
+  /**
+   * Sets whether a replay thread owns this change.
+   *
+   * @param owned {@code true} when a replay thread takes the change over, {@code false}
+   *              when its replay failed and the change must be delivered again
+   */
+  public void setOwned(boolean owned)
+  {
+    this.owned = owned;
+  }
+
+  /**
+   * Records that the replay of this change failed once more.
+   *
+   * @param nowMs
+   *          when it failed, on a clock which only moves forward
+   */
+  public void recordReplayFailure(long nowMs)
+  {
+    if (replayFailures == 0)
+    {
+      firstReplayFailureTimeMs = nowMs;
+    }
+    replayFailures++;
+  }
+
+  /**
+   * Returns how many times in a row the replay of this change failed.
+   *
+   * @return the number of failures, 0 when its replay never failed
+   */
+  public int getReplayFailures()
+  {
+    return replayFailures;
+  }
+
+  /**
+   * Returns how long the replay of this change has been failing.
+   *
+   * @param nowMs
+   *          the current time, on the clock {@link #recordReplayFailure(long)} was given
+   * @return the duration in milliseconds, 0 when its replay never failed
+   */
+  public long getReplayFailingForMs(long nowMs)
+  {
+    return replayFailures == 0 ? 0 : nowMs - firstReplayFailureTimeMs;
+  }
+
+  /**
    * Get the operation associated to the PendingChange.
    * @return the operation
    */

--
Gitblit v1.10.0