/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2014-2016 ForgeRock AS.
*/
package org.opends.server.replication.server.changelog.file;
import static org.opends.messages.ReplicationMessages.*;
import static org.opends.server.replication.server.changelog.api.DBCursor.PositionStrategy.*;
import static org.opends.server.util.StaticUtils.*;
import java.io.File;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import org.forgerock.i18n.LocalizableMessageBuilder;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.DurationUnit;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.util.Pair;
import org.forgerock.util.time.TimeService;
import org.opends.server.api.DirectoryThread;
import org.opends.server.backends.ChangelogBackend;
import org.opends.server.replication.common.CSN;
import org.opends.server.replication.common.MultiDomainServerState;
import org.opends.server.replication.common.ServerState;
import org.opends.server.replication.protocol.UpdateMsg;
import org.opends.server.replication.server.ChangelogState;
import org.opends.server.replication.server.ReplicationServer;
import org.opends.server.replication.server.changelog.api.ChangeNumberIndexDB;
import org.opends.server.replication.server.changelog.api.ChangeNumberIndexRecord;
import org.opends.server.replication.server.changelog.api.ChangelogDB;
import org.opends.server.replication.server.changelog.api.ChangelogException;
import org.opends.server.replication.server.changelog.api.DBCursor;
import org.opends.server.replication.server.changelog.api.DBCursor.CursorOptions;
import org.opends.server.replication.server.changelog.api.ReplicaId;
import org.opends.server.replication.server.changelog.api.ReplicationDomainDB;
import org.opends.server.replication.server.changelog.file.Log.RepositionableCursor;
import org.opends.server.types.DN;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.TimeThread;
/** Log file implementation of the ChangelogDB interface. */
public class FileChangelogDB implements ChangelogDB, ReplicationDomainDB
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/**
* This map contains the List of updates received from each LDAP server.
* <p>
* When removing a domainMap, code:
* <ol>
* <li>first get the domainMap</li>
* <li>synchronized on the domainMap</li>
* <li>remove the domainMap</li>
* <li>then check it's not null</li>
* <li>then close all inside</li>
* </ol>
* When creating a replicaDB, synchronize on the domainMap to avoid
* concurrent shutdown.
*/
private final ConcurrentMap<DN, ConcurrentMap<Integer, FileReplicaDB>> domainToReplicaDBs =
new ConcurrentHashMap<>();
private final ConcurrentSkipListMap<DN, CopyOnWriteArrayList<DomainDBCursor>> registeredDomainCursors =
new ConcurrentSkipListMap<>();
private final CopyOnWriteArrayList<MultiDomainDBCursor> registeredMultiDomainCursors = new CopyOnWriteArrayList<>();
private final ConcurrentSkipListMap<ReplicaId, CopyOnWriteArrayList<ReplicaCursor>> replicaCursors =
new ConcurrentSkipListMap<>();
private ReplicationEnvironment replicationEnv;
private final File dbDirectory;
/**
* The handler of the changelog database, the database stores the relation
* between a change number and the associated cookie.
* <p>
* @GuardedBy("cnIndexDBLock")
*/
private FileChangeNumberIndexDB cnIndexDB;
private final AtomicReference<ChangeNumberIndexer> cnIndexer = new AtomicReference<>();
/** Used for protecting {@link ChangeNumberIndexDB} related state. */
private final Object cnIndexDBLock = new Object();
/**
* The purge delay (in milliseconds). Records in the changelog DB that are
* older than this delay might be removed.
*/
private volatile long purgeDelayInMillis;
private final AtomicReference<ChangelogDBPurger> cnPurger = new AtomicReference<>();
/** The local replication server. */
private final ReplicationServer replicationServer;
private final AtomicBoolean shutdown = new AtomicBoolean();
private static final RepositionableCursor<CSN, UpdateMsg> EMPTY_CURSOR = Log.getEmptyCursor();
private static final DBCursor<UpdateMsg> EMPTY_CURSOR_REPLICA_DB =
new FileReplicaDBCursor(EMPTY_CURSOR, null, AFTER_MATCHING_KEY);
/**
* Creates a new changelog DB.
*
* @param replicationServer
* the local replication server.
* @param dbDirectoryPath
* the path where the changelog files reside.
* @throws ConfigException
* if a problem occurs opening the supplied directory
*/
public FileChangelogDB(final ReplicationServer replicationServer, String dbDirectoryPath)
throws ConfigException
{
this.replicationServer = replicationServer;
this.dbDirectory = makeDir(dbDirectoryPath);
}
private File makeDir(final String dbDirName) throws ConfigException
{
// Check that this path exists or create it.
final File dbDirectory = getFileForPath(dbDirName);
try
{
if (!dbDirectory.exists())
{
dbDirectory.mkdir();
}
return dbDirectory;
}
catch (Exception e)
{
final LocalizableMessageBuilder mb = new LocalizableMessageBuilder(
e.getLocalizedMessage()).append(" ").append(String.valueOf(dbDirectory));
throw new ConfigException(ERR_FILE_CHECK_CREATE_FAILED.get(mb.toString()), e);
}
}
private Map<Integer, FileReplicaDB> getDomainMap(final DN baseDN)
{
final Map<Integer, FileReplicaDB> domainMap = domainToReplicaDBs.get(baseDN);
if (domainMap != null)
{
return domainMap;
}
return Collections.emptyMap();
}
private FileReplicaDB getReplicaDB(final DN baseDN, final int serverId)
{
return getDomainMap(baseDN).get(serverId);
}
/**
* Returns a {@link FileReplicaDB}, possibly creating it.
*
* @param baseDN
* the baseDN for which to create a ReplicaDB
* @param serverId
* the serverId for which to create a ReplicaDB
* @param server
* the ReplicationServer
* @return a Pair with the FileReplicaDB and a boolean indicating whether it has been created
* @throws ChangelogException
* if a problem occurred with the database
*/
Pair<FileReplicaDB, Boolean> getOrCreateReplicaDB(final DN baseDN, final int serverId,
final ReplicationServer server) throws ChangelogException
{
while (!shutdown.get())
{
final ConcurrentMap<Integer, FileReplicaDB> domainMap = getExistingOrNewDomainMap(baseDN);
final Pair<FileReplicaDB, Boolean> result = getExistingOrNewReplicaDB(domainMap, serverId, baseDN, server);
if (result != null)
{
final Boolean dbWasCreated = result.getSecond();
if (dbWasCreated)
{ // new replicaDB => update all cursors with it
final List<DomainDBCursor> cursors = registeredDomainCursors.get(baseDN);
if (cursors != null && !cursors.isEmpty())
{
for (DomainDBCursor cursor : cursors)
{
cursor.addReplicaDB(serverId, null);
}
}
}
return result;
}
}
throw new ChangelogException(ERR_CANNOT_CREATE_REPLICA_DB_BECAUSE_CHANGELOG_DB_SHUTDOWN.get());
}
private ConcurrentMap<Integer, FileReplicaDB> getExistingOrNewDomainMap(final DN baseDN)
{
// happy path: the domainMap already exists
final ConcurrentMap<Integer, FileReplicaDB> currentValue = domainToReplicaDBs.get(baseDN);
if (currentValue != null)
{
return currentValue;
}
// unlucky, the domainMap does not exist: take the hit and create the
// newValue, even though the same could be done concurrently by another thread
final ConcurrentMap<Integer, FileReplicaDB> newValue = new ConcurrentHashMap<>();
final ConcurrentMap<Integer, FileReplicaDB> previousValue = domainToReplicaDBs.putIfAbsent(baseDN, newValue);
if (previousValue != null)
{
// there was already a value associated to the key, let's use it
return previousValue;
}
// we just created a new domain => update all cursors
for (MultiDomainDBCursor cursor : registeredMultiDomainCursors)
{
cursor.addDomain(baseDN, null);
}
return newValue;
}
private Pair<FileReplicaDB, Boolean> getExistingOrNewReplicaDB(final ConcurrentMap<Integer, FileReplicaDB> domainMap,
final int serverId, final DN baseDN, final ReplicationServer server) throws ChangelogException
{
// happy path: the replicaDB already exists
FileReplicaDB currentValue = domainMap.get(serverId);
if (currentValue != null)
{
return Pair.of(currentValue, false);
}
// unlucky, the replicaDB does not exist: take the hit and synchronize
// on the domainMap to create a new ReplicaDB
synchronized (domainMap)
{
// double-check
currentValue = domainMap.get(serverId);
if (currentValue != null)
{
return Pair.of(currentValue, false);
}
if (domainToReplicaDBs.get(baseDN) != domainMap)
{
// The domainMap could have been concurrently removed because
// 1) a shutdown was initiated or 2) an initialize was called.
// Return will allow the code to:
// 1) shutdown properly or 2) lazily recreate the replicaDB
return null;
}
final FileReplicaDB newDB = new FileReplicaDB(serverId, baseDN, server, replicationEnv);
domainMap.put(serverId, newDB);
return Pair.of(newDB, true);
}
}
@Override
public void initializeDB()
{
try
{
replicationEnv = new ReplicationEnvironment(dbDirectory.getAbsolutePath(), replicationServer, TimeService.SYSTEM);
final ChangelogState changelogState = replicationEnv.getChangelogState();
initializeToChangelogState(changelogState);
if (replicationServer.isChangeNumberEnabled())
{
startIndexer();
}
setPurgeDelay(replicationServer.getPurgeDelay());
}
catch (ChangelogException e)
{
logger.traceException(e);
logger.error(ERR_COULD_NOT_READ_DB, this.dbDirectory.getAbsolutePath(), e.getLocalizedMessage());
}
}
private void initializeToChangelogState(final ChangelogState changelogState)
throws ChangelogException
{
for (Map.Entry<DN, Long> entry : changelogState.getDomainToGenerationId().entrySet())
{
replicationServer.getReplicationServerDomain(entry.getKey(), true).initGenerationID(entry.getValue());
}
for (Map.Entry<DN, Set<Integer>> entry : changelogState.getDomainToServerIds().entrySet())
{
for (int serverId : entry.getValue())
{
getOrCreateReplicaDB(entry.getKey(), serverId, replicationServer);
}
}
}
private void shutdownChangeNumberIndexDB() throws ChangelogException
{
synchronized (cnIndexDBLock)
{
if (cnIndexDB != null)
{
cnIndexDB.shutdown();
}
}
}
@Override
public void shutdownDB() throws ChangelogException
{
if (!this.shutdown.compareAndSet(false, true))
{ // shutdown has already been initiated
return;
}
shutdownCNIndexerAndPurger();
// Remember the first exception because :
// - we want to try to remove everything we want to remove
// - then throw the first encountered exception
ChangelogException firstException = null;
// now we can safely shutdown all DBs
try
{
shutdownChangeNumberIndexDB();
}
catch (ChangelogException e)
{
firstException = e;
}
for (Iterator<ConcurrentMap<Integer, FileReplicaDB>> it =
this.domainToReplicaDBs.values().iterator(); it.hasNext();)
{
final ConcurrentMap<Integer, FileReplicaDB> domainMap = it.next();
synchronized (domainMap)
{
it.remove();
for (FileReplicaDB replicaDB : domainMap.values())
{
replicaDB.shutdown();
}
}
}
if (replicationEnv != null)
{
replicationEnv.shutdown();
}
if (firstException != null)
{
throw firstException;
}
}
private void shutdownCNIndexerAndPurger()
{
final ChangeNumberIndexer indexer = cnIndexer.getAndSet(null);
if (indexer != null)
{
indexer.initiateShutdown();
}
final ChangelogDBPurger purger = cnPurger.getAndSet(null);
if (purger != null)
{
purger.initiateShutdown();
}
// wait for shutdown of the threads holding cursors
try
{
if (indexer != null)
{
indexer.join();
}
if (purger != null)
{
purger.join();
}
}
catch (InterruptedException e)
{
// do nothing: we are already shutting down
}
}
/**
* Clears all records from the changelog (does not remove the changelog itself).
*
* @throws ChangelogException
* If an error occurs when clearing the changelog.
*/
public void clearDB() throws ChangelogException
{
if (!dbDirectory.exists())
{
return;
}
// Remember the first exception because :
// - we want to try to remove everything we want to remove
// - then throw the first encountered exception
ChangelogException firstException = null;
for (DN baseDN : this.domainToReplicaDBs.keySet())
{
removeDomain(baseDN);
}
synchronized (cnIndexDBLock)
{
if (cnIndexDB != null)
{
try
{
cnIndexDB.clear();
}
catch (ChangelogException e)
{
firstException = e;
}
try
{
shutdownChangeNumberIndexDB();
}
catch (ChangelogException e)
{
if (firstException == null)
{
firstException = e;
}
else
{
logger.traceException(e);
}
}
cnIndexDB = null;
}
}
if (firstException != null)
{
throw firstException;
}
}
@Override
public void removeDB() throws ChangelogException
{
shutdownDB();
StaticUtils.recursiveDelete(dbDirectory);
}
@Override
public ServerState getDomainOldestCSNs(DN baseDN)
{
final ServerState result = new ServerState();
for (FileReplicaDB replicaDB : getDomainMap(baseDN).values())
{
result.update(replicaDB.getOldestCSN());
}
return result;
}
@Override
public ServerState getDomainNewestCSNs(DN baseDN)
{
final ServerState result = new ServerState();
for (FileReplicaDB replicaDB : getDomainMap(baseDN).values())
{
result.update(replicaDB.getNewestCSN());
}
return result;
}
@Override
public void removeDomain(DN baseDN) throws ChangelogException
{
// Remember the first exception because :
// - we want to try to remove everything we want to remove
// - then throw the first encountered exception
ChangelogException firstException = null;
// 1- clear the replica DBs
Map<Integer, FileReplicaDB> domainMap = domainToReplicaDBs.get(baseDN);
if (domainMap != null)
{
final ChangeNumberIndexer indexer = this.cnIndexer.get();
if (indexer != null)
{
indexer.clear(baseDN);
}
synchronized (domainMap)
{
domainMap = domainToReplicaDBs.remove(baseDN);
for (FileReplicaDB replicaDB : domainMap.values())
{
try
{
replicaDB.clear();
}
catch (ChangelogException e)
{
firstException = e;
}
replicaDB.shutdown();
}
}
}
// 2- clear the changelogstate DB
try
{
replicationEnv.clearGenerationId(baseDN);
}
catch (ChangelogException e)
{
if (firstException == null)
{
firstException = e;
}
else
{
logger.traceException(e);
}
}
if (firstException != null)
{
throw firstException;
}
}
@Override
public void setPurgeDelay(final long purgeDelayInMillis)
{
this.purgeDelayInMillis = purgeDelayInMillis;
// Rotation time interval for CN Index DB log file
// needs to be a fraction of the purge delay
// to ensure there is at least one file to purge
replicationEnv.setCNIndexDBRotationInterval(purgeDelayInMillis / 2);
if (purgeDelayInMillis > 0)
{
startCNPurger();
}
else
{
final ChangelogDBPurger purgerToStop = cnPurger.getAndSet(null);
if (purgerToStop != null)
{ // stop this purger
purgerToStop.initiateShutdown();
}
}
}
private void startCNPurger()
{
final ChangelogDBPurger newPurger = new ChangelogDBPurger();
if (cnPurger.compareAndSet(null, newPurger))
{ // no purger was running, run this new one
newPurger.start();
}
else
{ // a purger was already running, just wake that one up
// to verify if some entries can be purged
final ChangelogDBPurger currentPurger = cnPurger.get();
synchronized (currentPurger)
{
currentPurger.notify();
}
}
}
@Override
public void setComputeChangeNumber(final boolean computeChangeNumber)
throws ChangelogException
{
if (computeChangeNumber)
{
startIndexer();
}
else
{
final ChangeNumberIndexer indexer = cnIndexer.getAndSet(null);
if (indexer != null)
{
indexer.initiateShutdown();
}
}
}
void resetChangeNumberIndex(long newFirstCN, DN baseDN, CSN newFirstCSN) throws ChangelogException
{
if (!replicationServer.isChangeNumberEnabled())
{
throw new ChangelogException(ERR_REPLICATION_CHANGE_NUMBER_DISABLED.get(baseDN));
}
if (!getDomainNewestCSNs(baseDN).cover(newFirstCSN))
{
throw new ChangelogException(ERR_CHANGELOG_RESET_CHANGE_NUMBER_CHANGE_NOT_PRESENT.get(newFirstCN, baseDN,
newFirstCSN));
}
if (getDomainOldestCSNs(baseDN).getCSN(newFirstCSN.getServerId()).isNewerThan(newFirstCSN))
{
throw new ChangelogException(ERR_CHANGELOG_RESET_CHANGE_NUMBER_CSN_TOO_OLD.get(newFirstCN, newFirstCSN));
}
shutdownCNIndexerAndPurger();
synchronized (cnIndexDBLock)
{
cnIndexDB.clearAndSetChangeNumber(newFirstCN);
cnIndexDB.addRecord(new ChangeNumberIndexRecord(newFirstCN, baseDN, newFirstCSN));
}
startIndexer();
if (purgeDelayInMillis > 0)
{
startCNPurger();
}
}
private void startIndexer()
{
final ChangeNumberIndexer indexer = new ChangeNumberIndexer(this, replicationEnv);
if (cnIndexer.compareAndSet(null, indexer))
{
indexer.start();
}
}
@Override
public ChangeNumberIndexDB getChangeNumberIndexDB()
{
synchronized (cnIndexDBLock)
{
if (cnIndexDB == null)
{
try
{
cnIndexDB = new FileChangeNumberIndexDB(this, replicationEnv);
}
catch (Exception e)
{
logger.traceException(e);
logger.error(ERR_CHANGENUMBER_DATABASE, e.getLocalizedMessage());
}
}
return cnIndexDB;
}
}
@Override
public ReplicationDomainDB getReplicationDomainDB()
{
return this;
}
@Override
public MultiDomainDBCursor getCursorFrom(final MultiDomainServerState startState, CursorOptions options)
throws ChangelogException
{
final Set<DN> excludedDomainDns = Collections.emptySet();
return getCursorFrom(startState, options, excludedDomainDns);
}
@Override
public MultiDomainDBCursor getCursorFrom(final MultiDomainServerState startState,
CursorOptions options, final Set<DN> excludedDomainDns) throws ChangelogException
{
final MultiDomainDBCursor cursor = new MultiDomainDBCursor(this, options);
registeredMultiDomainCursors.add(cursor);
for (DN baseDN : domainToReplicaDBs.keySet())
{
if (!excludedDomainDns.contains(baseDN))
{
cursor.addDomain(baseDN, startState.getServerState(baseDN));
}
}
return cursor;
}
@Override
public DBCursor<UpdateMsg> getCursorFrom(final DN baseDN, final ServerState startState, CursorOptions options)
throws ChangelogException
{
final DomainDBCursor cursor = newDomainDBCursor(baseDN, options);
for (int serverId : getDomainMap(baseDN).keySet())
{
// get the last already sent CSN from that server to get a cursor
final CSN lastCSN = startState != null ? startState.getCSN(serverId) : null;
cursor.addReplicaDB(serverId, lastCSN);
}
return cursor;
}
private DomainDBCursor newDomainDBCursor(final DN baseDN, final CursorOptions options)
{
final DomainDBCursor cursor = new DomainDBCursor(baseDN, this, options);
putCursor(registeredDomainCursors, baseDN, cursor);
return cursor;
}
private CSN getOfflineCSN(DN baseDN, int serverId, CSN startAfterCSN)
{
final MultiDomainServerState offlineReplicas =
replicationEnv.getChangelogState().getOfflineReplicas();
final CSN offlineCSN = offlineReplicas.getCSN(baseDN, serverId);
if (offlineCSN != null
&& (startAfterCSN == null || startAfterCSN.isOlderThan(offlineCSN)))
{
return offlineCSN;
}
return null;
}
@Override
public DBCursor<UpdateMsg> getCursorFrom(final DN baseDN, final int serverId, final CSN startCSN,
CursorOptions options) throws ChangelogException
{
final FileReplicaDB replicaDB = getReplicaDB(baseDN, serverId);
if (replicaDB != null)
{
final CSN actualStartCSN = startCSN != null ? startCSN : options.getDefaultCSN();
final DBCursor<UpdateMsg> cursor = replicaDB.generateCursorFrom(
actualStartCSN, options.getKeyMatchingStrategy(), options.getPositionStrategy());
final CSN offlineCSN = getOfflineCSN(baseDN, serverId, actualStartCSN);
final ReplicaId replicaId = ReplicaId.of(baseDN, serverId);
final ReplicaCursor replicaCursor = new ReplicaCursor(cursor, offlineCSN, replicaId, this);
putCursor(replicaCursors, replicaId, replicaCursor);
return replicaCursor;
}
return EMPTY_CURSOR_REPLICA_DB;
}
private <K, V> void putCursor(ConcurrentSkipListMap<K, CopyOnWriteArrayList<V>> map, final K key, final V cursor)
{
CopyOnWriteArrayList<V> cursors = map.get(key);
if (cursors == null)
{
cursors = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList<V> previousValue = map.putIfAbsent(key, cursors);
if (previousValue != null)
{
cursors = previousValue;
}
}
cursors.add(cursor);
}
@Override
public void unregisterCursor(final DBCursor<?> cursor)
{
if (cursor instanceof MultiDomainDBCursor)
{
registeredMultiDomainCursors.remove(cursor);
}
else if (cursor instanceof DomainDBCursor)
{
final DomainDBCursor domainCursor = (DomainDBCursor) cursor;
final List<DomainDBCursor> cursors = registeredDomainCursors.get(domainCursor.getBaseDN());
if (cursors != null)
{
cursors.remove(cursor);
}
}
else if (cursor instanceof ReplicaCursor)
{
final ReplicaCursor replicaCursor = (ReplicaCursor) cursor;
final List<ReplicaCursor> cursors = replicaCursors.get(replicaCursor.getReplicaId());
if (cursors != null)
{
cursors.remove(cursor);
}
}
}
@Override
public boolean publishUpdateMsg(final DN baseDN, final UpdateMsg updateMsg) throws ChangelogException
{
final CSN csn = updateMsg.getCSN();
final Pair<FileReplicaDB, Boolean> pair = getOrCreateReplicaDB(baseDN,
csn.getServerId(), replicationServer);
final FileReplicaDB replicaDB = pair.getFirst();
replicaDB.add(updateMsg);
ChangelogBackend.getInstance().notifyCookieEntryAdded(baseDN, updateMsg);
final ChangeNumberIndexer indexer = cnIndexer.get();
if (indexer != null)
{
notifyReplicaOnline(indexer, baseDN, csn.getServerId());
indexer.publishUpdateMsg(baseDN, updateMsg);
}
return pair.getSecond(); // replica DB was created
}
@Override
public void replicaHeartbeat(final DN baseDN, final CSN heartbeatCSN) throws ChangelogException
{
final ChangeNumberIndexer indexer = cnIndexer.get();
if (indexer != null)
{
notifyReplicaOnline(indexer, baseDN, heartbeatCSN.getServerId());
indexer.publishHeartbeat(baseDN, heartbeatCSN);
}
}
private void notifyReplicaOnline(final ChangeNumberIndexer indexer, final DN baseDN, final int serverId)
throws ChangelogException
{
if (indexer.isReplicaOffline(baseDN, serverId))
{
replicationEnv.notifyReplicaOnline(baseDN, serverId);
}
updateCursorsWithOfflineCSN(baseDN, serverId, null);
}
@Override
public void notifyReplicaOffline(final DN baseDN, final CSN offlineCSN) throws ChangelogException
{
replicationEnv.notifyReplicaOffline(baseDN, offlineCSN);
final ChangeNumberIndexer indexer = cnIndexer.get();
if (indexer != null)
{
indexer.replicaOffline(baseDN, offlineCSN);
}
updateCursorsWithOfflineCSN(baseDN, offlineCSN.getServerId(), offlineCSN);
}
private void updateCursorsWithOfflineCSN(final DN baseDN, final int serverId, final CSN offlineCSN)
{
final List<ReplicaCursor> cursors = replicaCursors.get(ReplicaId.of(baseDN, serverId));
if (cursors != null)
{
for (ReplicaCursor cursor : cursors)
{
cursor.setOfflineCSN(offlineCSN);
}
}
}
/**
* The thread purging the changelogDB on a regular interval. Records are
* purged from the changelogDB if they are older than a delay specified in
* seconds. The purge process works in two steps:
* <ol>
* <li>first purge the changeNumberIndexDB and retrieve information to drive
* replicaDBs purging</li>
* <li>proceed to purge each replicaDBs based on the information collected
* when purging the changeNumberIndexDB</li>
* </ol>
*/
private final class ChangelogDBPurger extends DirectoryThread
{
private static final int DEFAULT_SLEEP = 500;
protected ChangelogDBPurger()
{
super("Changelog DB purger");
}
@Override
public void run()
{
// initialize CNIndexDB
getChangeNumberIndexDB();
boolean canDisplayNothingToPurgeMsg = true;
while (!isShutdownInitiated())
{
try
{
final long purgeTimestamp = TimeThread.getTime() - purgeDelayInMillis;
final CSN purgeCSN = new CSN(purgeTimestamp, 0, 0);
final CSN oldestNotPurgedCSN;
if (!replicationServer.isChangeNumberEnabled() || !replicationServer.isECLEnabled())
{
oldestNotPurgedCSN = purgeCSN;
}
else
{
final FileChangeNumberIndexDB localCNIndexDB = cnIndexDB;
if (localCNIndexDB == null)
{ // shutdown has been initiated
return;
}
oldestNotPurgedCSN = localCNIndexDB.purgeUpTo(purgeCSN);
if (oldestNotPurgedCSN == null)
{ // shutdown may have been initiated...
// ... or change number index DB determined there is nothing to purge,
// wait for new changes to come in.
// Note we cannot sleep for as long as the purge delay
// (3 days default), because we might receive late updates
// that will have to be purged before the purge delay elapses.
// This can particularly happen in case of network partitions.
if (!isShutdownInitiated())
{
synchronized (this)
{
if (!isShutdownInitiated())
{
if (canDisplayNothingToPurgeMsg)
{
logger.trace("Nothing to purge, waiting for new changes");
canDisplayNothingToPurgeMsg = false;
}
wait(DEFAULT_SLEEP);
}
}
}
continue;
}
}
for (final Map<Integer, FileReplicaDB> domainMap : domainToReplicaDBs.values())
{
for (final FileReplicaDB replicaDB : domainMap.values())
{
replicaDB.purgeUpTo(oldestNotPurgedCSN);
}
}
if (!isShutdownInitiated())
{
synchronized (this)
{
if (!isShutdownInitiated())
{
final long sleepTime = computeSleepTimeUntilNextPurge(oldestNotPurgedCSN);
if (logger.isTraceEnabled())
{
tracePurgeDetails(purgeCSN, oldestNotPurgedCSN, sleepTime);
canDisplayNothingToPurgeMsg = true;
}
wait(sleepTime);
}
}
}
}
catch (InterruptedException e)
{
// shutdown initiated?
}
catch (Exception e)
{
logger.error(ERR_EXCEPTION_CHANGELOG_TRIM_FLUSH, stackTraceToSingleLineString(e));
if (replicationServer != null)
{
replicationServer.shutdown();
}
}
}
}
private void tracePurgeDetails(final CSN purgeCSN, final CSN oldestNotPurgedCSN, final long sleepTime)
{
if (purgeCSN.equals(oldestNotPurgedCSN.toStringUI()))
{
logger.trace("Purged up to %s. "
+ "now sleeping until next purge during %s",
purgeCSN.toStringUI(), DurationUnit.toString(sleepTime));
}
else
{
logger.trace("Asked to purge up to %s, actually purged up to %s (not included). "
+ "now sleeping until next purge during %s",
purgeCSN.toStringUI(), oldestNotPurgedCSN.toStringUI(), DurationUnit.toString(sleepTime));
}
}
private long computeSleepTimeUntilNextPurge(CSN notPurgedCSN)
{
final long nextPurgeTime = notPurgedCSN.getTime();
final long currentPurgeTime = TimeThread.getTime() - purgeDelayInMillis;
if (currentPurgeTime < nextPurgeTime)
{
// sleep till the next CSN to purge,
return nextPurgeTime - currentPurgeTime;
}
// wait a bit before purging more
return DEFAULT_SLEEP;
}
@Override
public void initiateShutdown()
{
super.initiateShutdown();
synchronized (this)
{
notify(); // wake up the purger thread for faster shutdown
}
}
}
}