/* * 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 2009 Sun Microsystems, Inc. * Portions Copyright 2011-2015 ForgeRock AS. * Portions Copyright 2026 3A Systems, LLC. */ package org.opends.server.replication.plugin; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map.Entry; import java.util.NoSuchElementException; import java.util.TreeMap; import java.util.concurrent.Callable; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.util.annotations.VisibleForTesting; import org.opends.server.replication.common.CSN; import org.opends.server.replication.common.CSNGenerator; import org.opends.server.replication.protocol.LDAPUpdateMsg; import org.opends.server.replication.protocol.ReplicaOfflineMsg; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.service.ReplicationDomain; import org.opends.server.types.operation.PluginOperation; /** * This class is used to store the list of local operations currently * in progress and not yet committed in the database. *
* It is used to make sure that operations are sent to the Replication Server * in the order defined by their CSN. It is also used to update the ServerState * at the appropriate time. *
* An object of this class is instantiated for each ReplicationDomain.
*/
class PendingChanges
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/**
* How many local changes a recovery keeps at most, and how many bytes their messages take at
* most: past either, it keeps none, and searches for them as for the changes done before it began.
*/
static final int MAX_KEPT_CHANGES = 10000;
static final long MAX_KEPT_BYTES = 32L * 1024 * 1024;
/** A map used to store the pending changes. */
private final TreeMap
* The message carries the newest CSN of the replica, so a change which is still in flight
* holds it back - and there is nobody left to publish it afterwards: the caller announces
* the replica offline while its service is being disabled, and the broker stops right after.
* Such a message is given up on rather than left queued, so that it is neither reported as
* sent nor published later on the session which follows.
*
* @return the CSN of the message which was published, or {@code null} if it could not be:
* a change which is still in flight holds it back, or the broker refused it
*/
public synchronized CSN putReplicaOfflineMsg()
{
final CSN offlineCSN = csnGenerator.newCSN();
final PendingChange pendingChange =
new PendingChange(offlineCSN, null, new ReplicaOfflineMsg(offlineCSN));
pendingChange.setCommitted(true);
pendingChanges.put(offlineCSN, pendingChange);
/*
* The message is the last change of the queue, so a push which did not reach it, or which
* the broker refused, reports another CSN or none.
*/
final boolean published = offlineCSN.equals(pushCommittedChanges());
// pushCommittedChanges() removes whatever it reached, so the message is still listed here
// if and only if a change before it held it back - it is dropped rather than left queued.
pendingChanges.remove(offlineCSN);
return published ? offlineCSN : null;
}
/**
* Push all committed local changes to the replicationServer service.
*
* @return the CSN of the last {@link ReplicaOfflineMsg} the replication service accepted, or
* {@code null} if none was pushed or the broker refused it. The announcement that a
* replica goes offline is the one message whose delivery the caller must know about:
* it is stored nowhere, so nothing publishes it again, while a change the broker
* refuses is republished from the historical information of its entry on the next
* session.
*/
synchronized CSN pushCommittedChanges()
{
CSN publishedOfflineCSN = null;
// peek the oldest change
Entry
* The local changes pushed meanwhile are kept for the recovery, which publishes them once it has
* published the ones done before it began. A recovery which is already running goes on, with what
* it kept: a session which starts while it runs starts no other.
*
* It is set without the lock of this list, which the start of a session must not take, but under
* the one the domain ends a recovery under as well: a recovery the session needs is not ended by
* one which was about to end.
*
* @param recovering
* The recovering status that must be set.
*/
public void setRecovering(boolean recovering)
{
recoveringOldChanges = recovering;
}
/**
* Tells whether the local changes are left to a recovery, which publishes them.
*
* It is read without the lock of this list, which the start of a session must not take: the
* session only learns from it that it needs a missing change publisher to end the recovery,
* and that one ends it under the lock.
*
* @return {@code true} if the local changes are left to a recovery
*/
boolean isRecovering()
{
return recoveringOldChanges;
}
/**
* Tells whether the recovery which runs keeps the local changes, and where its search of the
* historical information ends then.
*
* @return the recovery which keeps the local changes, or {@code null} if its search has to go up
* to the last local change: no recovery runs, or it kept too many changes to keep any
*/
synchronized KeptChanges keptChanges()
{
if (!recoveringOldChanges)
{
return null;
}
final Recovery running = recovery();
return running.kept != null ? new KeptChanges(running.searchUpTo) : null;
}
/**
* Returns the local changes the recovery which runs kept which come after the provided CSN, in
* order.
*
* @param csn
* where the recovery has got to
* @return the changes, or {@code null} if no recovery runs, or it kept too many changes to keep any
*/
synchronized List
* The recovery ends at once for every change pushed after it: a change which comes here once
* this list publishes again must find the broker taking it as well, or it is refused, while
* the domain state records it as done and no recovery is left to publish it. The recovery is
* therefore ended here, under the lock {@link #pushCommittedChanges()} takes, before any change
* can be pushed. What it kept is dropped with it.
*
* @param recovered
* The CSN of the last change that was published to the
* ReplicationServer.
* @param recoveryEnd
* Ends the recovery, unless a session which started meanwhile needs it, and has the
* broker take the changes again.
* @return A boolean indicating if the recovery is completed (false) or must
* continue (true).
*/
synchronized boolean recoveryUntil(CSN recovered, RecoveryEnd recoveryEnd)
{
final CSN lastLocalChange = domain.getLastLocalChange();
if (recoveringOldChanges && recovered != null && recovered.isNewerThanOrEqualTo(lastLocalChange))
{
recoveryEnd.end(() ->
{
recoveringOldChanges = false;
recovery = null;
});
}
return recoveringOldChanges;
}
/**
* Drops the provided local changes the recovery which runs kept: the data of the domain, read
* again, does not hold them - it was restored or imported since they were done - and the replicas
* would get changes this one does not hold.
*
* @param csns
* the CSNs of the changes to drop
*/
synchronized void dropKeptChanges(Collection
* A recovery republishes what the replication server misses from the historical information of
* the entries, and its search of it races the commits: a change committed while it searches is in
* the domain state, and not in what it found. The changes pushed once it began are kept instead,
* in order and with their messages, so that its search only has to cover the ones pushed before:
* up to the last of them, which no commit moves. A change kept is published as it was done, where
* a later change of the same values leaves nothing of it in the historical information, and a
* delete kept does not depend on the deletes the domain keeps aside, which are capped.
*
* What it keeps stays until it ends: a missing change publisher whose broker refused a change,
* or whose session was replaced by one whose replication server holds less, leaves them to the
* one which goes on, which may start from further back. Past {@link #maxKeptChanges} changes or
* {@link #MAX_KEPT_BYTES} bytes it keeps none, and the recovery searches for them from where it
* has got to, holding the local changes back at the end, as it does when nothing is kept.
*/
private final class Recovery
{
/** The last local change pushed before this recovery kept any: its search ends there. */
private final CSN searchUpTo;
/** The local changes pushed since, in order; {@code null} once there were too many. */
private TreeMap
* A round publishes the changes done when it began, and a change done while it runs is left
* to a round after it, so a recovery could chase the local changes for as long as they go on.
* The rounds run here are the last ones: what they publish is all there is, and nothing can be
* pushed between their end and the end of the recovery they decide. The local operations wait
* for them - for their CSN and for their push - for as long as publishing the changes done while
* the round before ran takes, where a publish in normal operation holds the same lock for one
* change.
*
* @param
* A collocated replication server can forward a {@link ReplicaOfflineMsg} as soon as it is on
* the wire, and its shutdown waits for that forward, so the announcement has to be in place
* before the message is published: one made afterwards is one the forward found nothing to
* clear, and the shutdown spends its whole grace period on a message which has already gone
* out. The broker may still refuse the message once it is announced, and then the
* announcement is withdrawn: what stays announced is what the broker reports as written.
*/
interface ReplicaOfflineAnnouncer
{
/**
* Announces that the replica goes offline at the provided CSN.
*
* @param offlineCSN
* the CSN of the ReplicaOfflineMsg which is about to be published
*/
void announce(CSN offlineCSN);
/**
* Withdraws the announcement of a message the broker refused: it was written to no session,
* so nobody will forward it.
*
* @param offlineCSN
* the CSN of the ReplicaOfflineMsg which was announced and not published
*/
void withdraw(CSN offlineCSN);
}
}