001/*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License").  You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
010 * or http://forgerock.org/license/CDDLv1.0.html.
011 * See the License for the specific language governing permissions
012 * and limitations under the License.
013 *
014 * When distributing Covered Code, include this CDDL HEADER in each
015 * file and include the License file at legal-notices/CDDLv1_0.txt.
016 * If applicable, add the following below this CDDL HEADER, with the
017 * fields enclosed by brackets "[]" replaced with your own identifying
018 * information:
019 *      Portions Copyright [yyyy] [name of copyright owner]
020 *
021 * CDDL HEADER END
022 *
023 *
024 *      Copyright 2014-2016 ForgeRock AS.
025 */
026package org.opends.server.replication.server.changelog.file;
027
028import static org.opends.messages.ReplicationMessages.*;
029import static org.opends.server.replication.server.changelog.api.DBCursor.PositionStrategy.*;
030import static org.opends.server.util.StaticUtils.*;
031
032import java.io.File;
033import java.util.Collections;
034import java.util.Iterator;
035import java.util.List;
036import java.util.Map;
037import java.util.Set;
038import java.util.concurrent.ConcurrentHashMap;
039import java.util.concurrent.ConcurrentMap;
040import java.util.concurrent.ConcurrentSkipListMap;
041import java.util.concurrent.CopyOnWriteArrayList;
042import java.util.concurrent.atomic.AtomicBoolean;
043import java.util.concurrent.atomic.AtomicReference;
044
045import org.forgerock.i18n.LocalizableMessageBuilder;
046import org.forgerock.i18n.slf4j.LocalizedLogger;
047import org.forgerock.opendj.config.DurationUnit;
048import org.forgerock.opendj.config.server.ConfigException;
049import org.forgerock.util.Pair;
050import org.forgerock.util.time.TimeService;
051import org.opends.server.api.DirectoryThread;
052import org.opends.server.backends.ChangelogBackend;
053import org.opends.server.replication.common.CSN;
054import org.opends.server.replication.common.MultiDomainServerState;
055import org.opends.server.replication.common.ServerState;
056import org.opends.server.replication.protocol.UpdateMsg;
057import org.opends.server.replication.server.ChangelogState;
058import org.opends.server.replication.server.ReplicationServer;
059import org.opends.server.replication.server.changelog.api.ChangeNumberIndexDB;
060import org.opends.server.replication.server.changelog.api.ChangeNumberIndexRecord;
061import org.opends.server.replication.server.changelog.api.ChangelogDB;
062import org.opends.server.replication.server.changelog.api.ChangelogException;
063import org.opends.server.replication.server.changelog.api.DBCursor;
064import org.opends.server.replication.server.changelog.api.DBCursor.CursorOptions;
065import org.opends.server.replication.server.changelog.api.ReplicaId;
066import org.opends.server.replication.server.changelog.api.ReplicationDomainDB;
067import org.opends.server.replication.server.changelog.file.Log.RepositionableCursor;
068import org.opends.server.types.DN;
069import org.opends.server.util.StaticUtils;
070import org.opends.server.util.TimeThread;
071
072/** Log file implementation of the ChangelogDB interface. */
073public class FileChangelogDB implements ChangelogDB, ReplicationDomainDB
074{
075  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
076
077  /**
078   * This map contains the List of updates received from each LDAP server.
079   * <p>
080   * When removing a domainMap, code:
081   * <ol>
082   * <li>first get the domainMap</li>
083   * <li>synchronized on the domainMap</li>
084   * <li>remove the domainMap</li>
085   * <li>then check it's not null</li>
086   * <li>then close all inside</li>
087   * </ol>
088   * When creating a replicaDB, synchronize on the domainMap to avoid
089   * concurrent shutdown.
090   */
091  private final ConcurrentMap<DN, ConcurrentMap<Integer, FileReplicaDB>> domainToReplicaDBs =
092      new ConcurrentHashMap<>();
093  private final ConcurrentSkipListMap<DN, CopyOnWriteArrayList<DomainDBCursor>> registeredDomainCursors =
094      new ConcurrentSkipListMap<>();
095  private final CopyOnWriteArrayList<MultiDomainDBCursor> registeredMultiDomainCursors = new CopyOnWriteArrayList<>();
096  private final ConcurrentSkipListMap<ReplicaId, CopyOnWriteArrayList<ReplicaCursor>> replicaCursors =
097      new ConcurrentSkipListMap<>();
098  private ReplicationEnvironment replicationEnv;
099  private final File dbDirectory;
100
101  /**
102   * The handler of the changelog database, the database stores the relation
103   * between a change number and the associated cookie.
104   * <p>
105   * @GuardedBy("cnIndexDBLock")
106   */
107  private FileChangeNumberIndexDB cnIndexDB;
108  private final AtomicReference<ChangeNumberIndexer> cnIndexer = new AtomicReference<>();
109
110  /** Used for protecting {@link ChangeNumberIndexDB} related state. */
111  private final Object cnIndexDBLock = new Object();
112
113  /**
114   * The purge delay (in milliseconds). Records in the changelog DB that are
115   * older than this delay might be removed.
116   */
117  private volatile long purgeDelayInMillis;
118  private final AtomicReference<ChangelogDBPurger> cnPurger = new AtomicReference<>();
119
120  /** The local replication server. */
121  private final ReplicationServer replicationServer;
122  private final AtomicBoolean shutdown = new AtomicBoolean();
123
124  private static final RepositionableCursor<CSN, UpdateMsg> EMPTY_CURSOR = Log.getEmptyCursor();
125  private static final DBCursor<UpdateMsg> EMPTY_CURSOR_REPLICA_DB =
126      new FileReplicaDBCursor(EMPTY_CURSOR, null, AFTER_MATCHING_KEY);
127
128  /**
129   * Creates a new changelog DB.
130   *
131   * @param replicationServer
132   *          the local replication server.
133   * @param dbDirectoryPath
134   *          the path where the changelog files reside.
135   * @throws ConfigException
136   *           if a problem occurs opening the supplied directory
137   */
138  public FileChangelogDB(final ReplicationServer replicationServer, String dbDirectoryPath)
139      throws ConfigException
140  {
141    this.replicationServer = replicationServer;
142    this.dbDirectory = makeDir(dbDirectoryPath);
143  }
144
145  private File makeDir(final String dbDirName) throws ConfigException
146  {
147    // Check that this path exists or create it.
148    final File dbDirectory = getFileForPath(dbDirName);
149    try
150    {
151      if (!dbDirectory.exists())
152      {
153        dbDirectory.mkdir();
154      }
155      return dbDirectory;
156    }
157    catch (Exception e)
158    {
159      final LocalizableMessageBuilder mb = new LocalizableMessageBuilder(
160          e.getLocalizedMessage()).append(" ").append(String.valueOf(dbDirectory));
161      throw new ConfigException(ERR_FILE_CHECK_CREATE_FAILED.get(mb.toString()), e);
162    }
163  }
164
165  private Map<Integer, FileReplicaDB> getDomainMap(final DN baseDN)
166  {
167    final Map<Integer, FileReplicaDB> domainMap = domainToReplicaDBs.get(baseDN);
168    if (domainMap != null)
169    {
170      return domainMap;
171    }
172    return Collections.emptyMap();
173  }
174
175  private FileReplicaDB getReplicaDB(final DN baseDN, final int serverId)
176  {
177    return getDomainMap(baseDN).get(serverId);
178  }
179
180  /**
181   * Returns a {@link FileReplicaDB}, possibly creating it.
182   *
183   * @param baseDN
184   *          the baseDN for which to create a ReplicaDB
185   * @param serverId
186   *          the serverId for which to create a ReplicaDB
187   * @param server
188   *          the ReplicationServer
189   * @return a Pair with the FileReplicaDB and a boolean indicating whether it has been created
190   * @throws ChangelogException
191   *           if a problem occurred with the database
192   */
193  Pair<FileReplicaDB, Boolean> getOrCreateReplicaDB(final DN baseDN, final int serverId,
194      final ReplicationServer server) throws ChangelogException
195  {
196    while (!shutdown.get())
197    {
198      final ConcurrentMap<Integer, FileReplicaDB> domainMap = getExistingOrNewDomainMap(baseDN);
199      final Pair<FileReplicaDB, Boolean> result = getExistingOrNewReplicaDB(domainMap, serverId, baseDN, server);
200      if (result != null)
201      {
202        final Boolean dbWasCreated = result.getSecond();
203        if (dbWasCreated)
204        { // new replicaDB => update all cursors with it
205          final List<DomainDBCursor> cursors = registeredDomainCursors.get(baseDN);
206          if (cursors != null && !cursors.isEmpty())
207          {
208            for (DomainDBCursor cursor : cursors)
209            {
210              cursor.addReplicaDB(serverId, null);
211            }
212          }
213        }
214
215        return result;
216      }
217    }
218    throw new ChangelogException(ERR_CANNOT_CREATE_REPLICA_DB_BECAUSE_CHANGELOG_DB_SHUTDOWN.get());
219  }
220
221  private ConcurrentMap<Integer, FileReplicaDB> getExistingOrNewDomainMap(final DN baseDN)
222  {
223    // happy path: the domainMap already exists
224    final ConcurrentMap<Integer, FileReplicaDB> currentValue = domainToReplicaDBs.get(baseDN);
225    if (currentValue != null)
226    {
227      return currentValue;
228    }
229
230    // unlucky, the domainMap does not exist: take the hit and create the
231    // newValue, even though the same could be done concurrently by another thread
232    final ConcurrentMap<Integer, FileReplicaDB> newValue = new ConcurrentHashMap<>();
233    final ConcurrentMap<Integer, FileReplicaDB> previousValue = domainToReplicaDBs.putIfAbsent(baseDN, newValue);
234    if (previousValue != null)
235    {
236      // there was already a value associated to the key, let's use it
237      return previousValue;
238    }
239
240    // we just created a new domain => update all cursors
241    for (MultiDomainDBCursor cursor : registeredMultiDomainCursors)
242    {
243      cursor.addDomain(baseDN, null);
244    }
245    return newValue;
246  }
247
248  private Pair<FileReplicaDB, Boolean> getExistingOrNewReplicaDB(final ConcurrentMap<Integer, FileReplicaDB> domainMap,
249      final int serverId, final DN baseDN, final ReplicationServer server) throws ChangelogException
250  {
251    // happy path: the replicaDB already exists
252    FileReplicaDB currentValue = domainMap.get(serverId);
253    if (currentValue != null)
254    {
255      return Pair.of(currentValue, false);
256    }
257
258    // unlucky, the replicaDB does not exist: take the hit and synchronize
259    // on the domainMap to create a new ReplicaDB
260    synchronized (domainMap)
261    {
262      // double-check
263      currentValue = domainMap.get(serverId);
264      if (currentValue != null)
265      {
266        return Pair.of(currentValue, false);
267      }
268
269      if (domainToReplicaDBs.get(baseDN) != domainMap)
270      {
271        // The domainMap could have been concurrently removed because
272        // 1) a shutdown was initiated or 2) an initialize was called.
273        // Return will allow the code to:
274        // 1) shutdown properly or 2) lazily recreate the replicaDB
275        return null;
276      }
277
278      final FileReplicaDB newDB = new FileReplicaDB(serverId, baseDN, server, replicationEnv);
279      domainMap.put(serverId, newDB);
280      return Pair.of(newDB, true);
281    }
282  }
283
284  @Override
285  public void initializeDB()
286  {
287    try
288    {
289      replicationEnv = new ReplicationEnvironment(dbDirectory.getAbsolutePath(), replicationServer, TimeService.SYSTEM);
290      final ChangelogState changelogState = replicationEnv.getChangelogState();
291      initializeToChangelogState(changelogState);
292      if (replicationServer.isChangeNumberEnabled())
293      {
294        startIndexer();
295      }
296      setPurgeDelay(replicationServer.getPurgeDelay());
297    }
298    catch (ChangelogException e)
299    {
300      logger.traceException(e);
301      logger.error(ERR_COULD_NOT_READ_DB, this.dbDirectory.getAbsolutePath(), e.getLocalizedMessage());
302    }
303  }
304
305  private void initializeToChangelogState(final ChangelogState changelogState)
306      throws ChangelogException
307  {
308    for (Map.Entry<DN, Long> entry : changelogState.getDomainToGenerationId().entrySet())
309    {
310      replicationServer.getReplicationServerDomain(entry.getKey(), true).initGenerationID(entry.getValue());
311    }
312    for (Map.Entry<DN, Set<Integer>> entry : changelogState.getDomainToServerIds().entrySet())
313    {
314      for (int serverId : entry.getValue())
315      {
316        getOrCreateReplicaDB(entry.getKey(), serverId, replicationServer);
317      }
318    }
319  }
320
321  private void shutdownChangeNumberIndexDB() throws ChangelogException
322  {
323    synchronized (cnIndexDBLock)
324    {
325      if (cnIndexDB != null)
326      {
327        cnIndexDB.shutdown();
328      }
329    }
330  }
331
332  @Override
333  public void shutdownDB() throws ChangelogException
334  {
335    if (!this.shutdown.compareAndSet(false, true))
336    { // shutdown has already been initiated
337      return;
338    }
339
340    shutdownCNIndexerAndPurger();
341
342    // Remember the first exception because :
343    // - we want to try to remove everything we want to remove
344    // - then throw the first encountered exception
345    ChangelogException firstException = null;
346
347    // now we can safely shutdown all DBs
348    try
349    {
350      shutdownChangeNumberIndexDB();
351    }
352    catch (ChangelogException e)
353    {
354      firstException = e;
355    }
356
357    for (Iterator<ConcurrentMap<Integer, FileReplicaDB>> it =
358        this.domainToReplicaDBs.values().iterator(); it.hasNext();)
359    {
360      final ConcurrentMap<Integer, FileReplicaDB> domainMap = it.next();
361      synchronized (domainMap)
362      {
363        it.remove();
364        for (FileReplicaDB replicaDB : domainMap.values())
365        {
366          replicaDB.shutdown();
367        }
368      }
369    }
370    if (replicationEnv != null)
371    {
372      replicationEnv.shutdown();
373    }
374
375    if (firstException != null)
376    {
377      throw firstException;
378    }
379  }
380
381  private void shutdownCNIndexerAndPurger()
382  {
383    final ChangeNumberIndexer indexer = cnIndexer.getAndSet(null);
384    if (indexer != null)
385    {
386      indexer.initiateShutdown();
387    }
388    final ChangelogDBPurger purger = cnPurger.getAndSet(null);
389    if (purger != null)
390    {
391      purger.initiateShutdown();
392    }
393
394    // wait for shutdown of the threads holding cursors
395    try
396    {
397      if (indexer != null)
398      {
399        indexer.join();
400      }
401      if (purger != null)
402      {
403        purger.join();
404      }
405    }
406    catch (InterruptedException e)
407    {
408      // do nothing: we are already shutting down
409    }
410  }
411
412  /**
413   * Clears all records from the changelog (does not remove the changelog itself).
414   *
415   * @throws ChangelogException
416   *           If an error occurs when clearing the changelog.
417   */
418  public void clearDB() throws ChangelogException
419  {
420    if (!dbDirectory.exists())
421    {
422      return;
423    }
424
425    // Remember the first exception because :
426    // - we want to try to remove everything we want to remove
427    // - then throw the first encountered exception
428    ChangelogException firstException = null;
429
430    for (DN baseDN : this.domainToReplicaDBs.keySet())
431    {
432      removeDomain(baseDN);
433    }
434
435    synchronized (cnIndexDBLock)
436    {
437      if (cnIndexDB != null)
438      {
439        try
440        {
441          cnIndexDB.clear();
442        }
443        catch (ChangelogException e)
444        {
445          firstException = e;
446        }
447
448        try
449        {
450          shutdownChangeNumberIndexDB();
451        }
452        catch (ChangelogException e)
453        {
454          if (firstException == null)
455          {
456            firstException = e;
457          }
458          else
459          {
460            logger.traceException(e);
461          }
462        }
463
464        cnIndexDB = null;
465      }
466    }
467
468    if (firstException != null)
469    {
470      throw firstException;
471    }
472  }
473
474  @Override
475  public void removeDB() throws ChangelogException
476  {
477    shutdownDB();
478    StaticUtils.recursiveDelete(dbDirectory);
479  }
480
481  @Override
482  public ServerState getDomainOldestCSNs(DN baseDN)
483  {
484    final ServerState result = new ServerState();
485    for (FileReplicaDB replicaDB : getDomainMap(baseDN).values())
486    {
487      result.update(replicaDB.getOldestCSN());
488    }
489    return result;
490  }
491
492  @Override
493  public ServerState getDomainNewestCSNs(DN baseDN)
494  {
495    final ServerState result = new ServerState();
496    for (FileReplicaDB replicaDB : getDomainMap(baseDN).values())
497    {
498      result.update(replicaDB.getNewestCSN());
499    }
500    return result;
501  }
502
503  @Override
504  public void removeDomain(DN baseDN) throws ChangelogException
505  {
506    // Remember the first exception because :
507    // - we want to try to remove everything we want to remove
508    // - then throw the first encountered exception
509    ChangelogException firstException = null;
510
511    // 1- clear the replica DBs
512    Map<Integer, FileReplicaDB> domainMap = domainToReplicaDBs.get(baseDN);
513    if (domainMap != null)
514    {
515      final ChangeNumberIndexer indexer = this.cnIndexer.get();
516      if (indexer != null)
517      {
518        indexer.clear(baseDN);
519      }
520      synchronized (domainMap)
521      {
522        domainMap = domainToReplicaDBs.remove(baseDN);
523        for (FileReplicaDB replicaDB : domainMap.values())
524        {
525          try
526          {
527            replicaDB.clear();
528          }
529          catch (ChangelogException e)
530          {
531            firstException = e;
532          }
533          replicaDB.shutdown();
534        }
535      }
536    }
537
538
539    // 2- clear the changelogstate DB
540    try
541    {
542      replicationEnv.clearGenerationId(baseDN);
543    }
544    catch (ChangelogException e)
545    {
546      if (firstException == null)
547      {
548        firstException = e;
549      }
550      else
551      {
552        logger.traceException(e);
553      }
554    }
555
556    if (firstException != null)
557    {
558      throw firstException;
559    }
560  }
561
562  @Override
563  public void setPurgeDelay(final long purgeDelayInMillis)
564  {
565    this.purgeDelayInMillis = purgeDelayInMillis;
566
567    // Rotation time interval for CN Index DB log file
568    // needs to be a fraction of the purge delay
569    // to ensure there is at least one file to purge
570    replicationEnv.setCNIndexDBRotationInterval(purgeDelayInMillis / 2);
571
572    if (purgeDelayInMillis > 0)
573    {
574      startCNPurger();
575    }
576    else
577    {
578      final ChangelogDBPurger purgerToStop = cnPurger.getAndSet(null);
579      if (purgerToStop != null)
580      { // stop this purger
581        purgerToStop.initiateShutdown();
582      }
583    }
584  }
585
586  private void startCNPurger()
587  {
588    final ChangelogDBPurger newPurger = new ChangelogDBPurger();
589    if (cnPurger.compareAndSet(null, newPurger))
590    { // no purger was running, run this new one
591      newPurger.start();
592    }
593    else
594    { // a purger was already running, just wake that one up
595      // to verify if some entries can be purged
596      final ChangelogDBPurger currentPurger = cnPurger.get();
597      synchronized (currentPurger)
598      {
599        currentPurger.notify();
600      }
601    }
602  }
603
604  @Override
605  public void setComputeChangeNumber(final boolean computeChangeNumber)
606      throws ChangelogException
607  {
608    if (computeChangeNumber)
609    {
610      startIndexer();
611    }
612    else
613    {
614      final ChangeNumberIndexer indexer = cnIndexer.getAndSet(null);
615      if (indexer != null)
616      {
617        indexer.initiateShutdown();
618      }
619    }
620  }
621
622  void resetChangeNumberIndex(long newFirstCN, DN baseDN, CSN newFirstCSN) throws ChangelogException
623  {
624    if (!replicationServer.isChangeNumberEnabled())
625    {
626      throw new ChangelogException(ERR_REPLICATION_CHANGE_NUMBER_DISABLED.get(baseDN));
627    }
628    if (!getDomainNewestCSNs(baseDN).cover(newFirstCSN))
629    {
630      throw new ChangelogException(ERR_CHANGELOG_RESET_CHANGE_NUMBER_CHANGE_NOT_PRESENT.get(newFirstCN, baseDN,
631          newFirstCSN));
632    }
633    if (getDomainOldestCSNs(baseDN).getCSN(newFirstCSN.getServerId()).isNewerThan(newFirstCSN))
634    {
635      throw new ChangelogException(ERR_CHANGELOG_RESET_CHANGE_NUMBER_CSN_TOO_OLD.get(newFirstCN, newFirstCSN));
636    }
637
638    shutdownCNIndexerAndPurger();
639    synchronized (cnIndexDBLock)
640    {
641      cnIndexDB.clearAndSetChangeNumber(newFirstCN);
642      cnIndexDB.addRecord(new ChangeNumberIndexRecord(newFirstCN, baseDN, newFirstCSN));
643    }
644    startIndexer();
645    if (purgeDelayInMillis > 0)
646    {
647      startCNPurger();
648    }
649  }
650
651  private void startIndexer()
652  {
653    final ChangeNumberIndexer indexer = new ChangeNumberIndexer(this, replicationEnv);
654    if (cnIndexer.compareAndSet(null, indexer))
655    {
656      indexer.start();
657    }
658  }
659
660  @Override
661  public ChangeNumberIndexDB getChangeNumberIndexDB()
662  {
663    synchronized (cnIndexDBLock)
664    {
665      if (cnIndexDB == null)
666      {
667        try
668        {
669          cnIndexDB = new FileChangeNumberIndexDB(this, replicationEnv);
670        }
671        catch (Exception e)
672        {
673          logger.traceException(e);
674          logger.error(ERR_CHANGENUMBER_DATABASE, e.getLocalizedMessage());
675        }
676      }
677      return cnIndexDB;
678    }
679  }
680
681  @Override
682  public ReplicationDomainDB getReplicationDomainDB()
683  {
684    return this;
685  }
686
687  @Override
688  public MultiDomainDBCursor getCursorFrom(final MultiDomainServerState startState, CursorOptions options)
689      throws ChangelogException
690  {
691    final Set<DN> excludedDomainDns = Collections.emptySet();
692    return getCursorFrom(startState, options, excludedDomainDns);
693  }
694
695  @Override
696  public MultiDomainDBCursor getCursorFrom(final MultiDomainServerState startState,
697      CursorOptions options, final Set<DN> excludedDomainDns) throws ChangelogException
698  {
699    final MultiDomainDBCursor cursor = new MultiDomainDBCursor(this, options);
700    registeredMultiDomainCursors.add(cursor);
701    for (DN baseDN : domainToReplicaDBs.keySet())
702    {
703      if (!excludedDomainDns.contains(baseDN))
704      {
705        cursor.addDomain(baseDN, startState.getServerState(baseDN));
706      }
707    }
708    return cursor;
709  }
710
711  @Override
712  public DBCursor<UpdateMsg> getCursorFrom(final DN baseDN, final ServerState startState, CursorOptions options)
713      throws ChangelogException
714  {
715    final DomainDBCursor cursor = newDomainDBCursor(baseDN, options);
716    for (int serverId : getDomainMap(baseDN).keySet())
717    {
718      // get the last already sent CSN from that server to get a cursor
719      final CSN lastCSN = startState != null ? startState.getCSN(serverId) : null;
720      cursor.addReplicaDB(serverId, lastCSN);
721    }
722    return cursor;
723  }
724
725  private DomainDBCursor newDomainDBCursor(final DN baseDN, final CursorOptions options)
726  {
727    final DomainDBCursor cursor = new DomainDBCursor(baseDN, this, options);
728    putCursor(registeredDomainCursors, baseDN, cursor);
729    return cursor;
730  }
731
732  private CSN getOfflineCSN(DN baseDN, int serverId, CSN startAfterCSN)
733  {
734    final MultiDomainServerState offlineReplicas =
735        replicationEnv.getChangelogState().getOfflineReplicas();
736    final CSN offlineCSN = offlineReplicas.getCSN(baseDN, serverId);
737    if (offlineCSN != null
738        && (startAfterCSN == null || startAfterCSN.isOlderThan(offlineCSN)))
739    {
740      return offlineCSN;
741    }
742    return null;
743  }
744
745  @Override
746  public DBCursor<UpdateMsg> getCursorFrom(final DN baseDN, final int serverId, final CSN startCSN,
747      CursorOptions options) throws ChangelogException
748  {
749    final FileReplicaDB replicaDB = getReplicaDB(baseDN, serverId);
750    if (replicaDB != null)
751    {
752      final CSN actualStartCSN = startCSN != null ? startCSN : options.getDefaultCSN();
753      final DBCursor<UpdateMsg> cursor = replicaDB.generateCursorFrom(
754          actualStartCSN, options.getKeyMatchingStrategy(), options.getPositionStrategy());
755      final CSN offlineCSN = getOfflineCSN(baseDN, serverId, actualStartCSN);
756      final ReplicaId replicaId = ReplicaId.of(baseDN, serverId);
757      final ReplicaCursor replicaCursor = new ReplicaCursor(cursor, offlineCSN, replicaId, this);
758
759      putCursor(replicaCursors, replicaId, replicaCursor);
760
761      return replicaCursor;
762    }
763    return EMPTY_CURSOR_REPLICA_DB;
764  }
765
766  private <K, V> void putCursor(ConcurrentSkipListMap<K, CopyOnWriteArrayList<V>> map, final K key, final V cursor)
767  {
768    CopyOnWriteArrayList<V> cursors = map.get(key);
769    if (cursors == null)
770    {
771      cursors = new CopyOnWriteArrayList<>();
772      CopyOnWriteArrayList<V> previousValue = map.putIfAbsent(key, cursors);
773      if (previousValue != null)
774      {
775        cursors = previousValue;
776      }
777    }
778    cursors.add(cursor);
779  }
780
781  @Override
782  public void unregisterCursor(final DBCursor<?> cursor)
783  {
784    if (cursor instanceof MultiDomainDBCursor)
785    {
786      registeredMultiDomainCursors.remove(cursor);
787    }
788    else if (cursor instanceof DomainDBCursor)
789    {
790      final DomainDBCursor domainCursor = (DomainDBCursor) cursor;
791      final List<DomainDBCursor> cursors = registeredDomainCursors.get(domainCursor.getBaseDN());
792      if (cursors != null)
793      {
794        cursors.remove(cursor);
795      }
796    }
797    else if (cursor instanceof ReplicaCursor)
798    {
799      final ReplicaCursor replicaCursor = (ReplicaCursor) cursor;
800      final List<ReplicaCursor> cursors = replicaCursors.get(replicaCursor.getReplicaId());
801      if (cursors != null)
802      {
803        cursors.remove(cursor);
804      }
805    }
806  }
807
808  @Override
809  public boolean publishUpdateMsg(final DN baseDN, final UpdateMsg updateMsg) throws ChangelogException
810  {
811    final CSN csn = updateMsg.getCSN();
812    final Pair<FileReplicaDB, Boolean> pair = getOrCreateReplicaDB(baseDN,
813        csn.getServerId(), replicationServer);
814    final FileReplicaDB replicaDB = pair.getFirst();
815    replicaDB.add(updateMsg);
816
817    ChangelogBackend.getInstance().notifyCookieEntryAdded(baseDN, updateMsg);
818
819    final ChangeNumberIndexer indexer = cnIndexer.get();
820    if (indexer != null)
821    {
822      notifyReplicaOnline(indexer, baseDN, csn.getServerId());
823      indexer.publishUpdateMsg(baseDN, updateMsg);
824    }
825    return pair.getSecond(); // replica DB was created
826  }
827
828  @Override
829  public void replicaHeartbeat(final DN baseDN, final CSN heartbeatCSN) throws ChangelogException
830  {
831    final ChangeNumberIndexer indexer = cnIndexer.get();
832    if (indexer != null)
833    {
834      notifyReplicaOnline(indexer, baseDN, heartbeatCSN.getServerId());
835      indexer.publishHeartbeat(baseDN, heartbeatCSN);
836    }
837  }
838
839  private void notifyReplicaOnline(final ChangeNumberIndexer indexer, final DN baseDN, final int serverId)
840      throws ChangelogException
841  {
842    if (indexer.isReplicaOffline(baseDN, serverId))
843    {
844      replicationEnv.notifyReplicaOnline(baseDN, serverId);
845    }
846    updateCursorsWithOfflineCSN(baseDN, serverId, null);
847  }
848
849  @Override
850  public void notifyReplicaOffline(final DN baseDN, final CSN offlineCSN) throws ChangelogException
851  {
852    replicationEnv.notifyReplicaOffline(baseDN, offlineCSN);
853    final ChangeNumberIndexer indexer = cnIndexer.get();
854    if (indexer != null)
855    {
856      indexer.replicaOffline(baseDN, offlineCSN);
857    }
858    updateCursorsWithOfflineCSN(baseDN, offlineCSN.getServerId(), offlineCSN);
859  }
860
861  private void updateCursorsWithOfflineCSN(final DN baseDN, final int serverId, final CSN offlineCSN)
862  {
863    final List<ReplicaCursor> cursors = replicaCursors.get(ReplicaId.of(baseDN, serverId));
864    if (cursors != null)
865    {
866      for (ReplicaCursor cursor : cursors)
867      {
868        cursor.setOfflineCSN(offlineCSN);
869      }
870    }
871  }
872
873  /**
874   * The thread purging the changelogDB on a regular interval. Records are
875   * purged from the changelogDB if they are older than a delay specified in
876   * seconds. The purge process works in two steps:
877   * <ol>
878   * <li>first purge the changeNumberIndexDB and retrieve information to drive
879   * replicaDBs purging</li>
880   * <li>proceed to purge each replicaDBs based on the information collected
881   * when purging the changeNumberIndexDB</li>
882   * </ol>
883   */
884  private final class ChangelogDBPurger extends DirectoryThread
885  {
886    private static final int DEFAULT_SLEEP = 500;
887
888    protected ChangelogDBPurger()
889    {
890      super("Changelog DB purger");
891    }
892
893    @Override
894    public void run()
895    {
896      // initialize CNIndexDB
897      getChangeNumberIndexDB();
898      boolean canDisplayNothingToPurgeMsg = true;
899      while (!isShutdownInitiated())
900      {
901        try
902        {
903          final long purgeTimestamp = TimeThread.getTime() - purgeDelayInMillis;
904          final CSN purgeCSN = new CSN(purgeTimestamp, 0, 0);
905          final CSN oldestNotPurgedCSN;
906
907          if (!replicationServer.isChangeNumberEnabled() || !replicationServer.isECLEnabled())
908          {
909            oldestNotPurgedCSN = purgeCSN;
910          }
911          else
912          {
913            final FileChangeNumberIndexDB localCNIndexDB = cnIndexDB;
914            if (localCNIndexDB == null)
915            { // shutdown has been initiated
916              return;
917            }
918
919            oldestNotPurgedCSN = localCNIndexDB.purgeUpTo(purgeCSN);
920            if (oldestNotPurgedCSN == null)
921            { // shutdown may have been initiated...
922              // ... or change number index DB determined there is nothing to purge,
923              // wait for new changes to come in.
924
925              // Note we cannot sleep for as long as the purge delay
926              // (3 days default), because we might receive late updates
927              // that will have to be purged before the purge delay elapses.
928              // This can particularly happen in case of network partitions.
929              if (!isShutdownInitiated())
930              {
931                synchronized (this)
932                {
933                  if (!isShutdownInitiated())
934                  {
935                    if (canDisplayNothingToPurgeMsg)
936                    {
937                      logger.trace("Nothing to purge, waiting for new changes");
938                      canDisplayNothingToPurgeMsg = false;
939                    }
940                    wait(DEFAULT_SLEEP);
941                  }
942                }
943              }
944              continue;
945            }
946          }
947
948          for (final Map<Integer, FileReplicaDB> domainMap : domainToReplicaDBs.values())
949          {
950            for (final FileReplicaDB replicaDB : domainMap.values())
951            {
952              replicaDB.purgeUpTo(oldestNotPurgedCSN);
953            }
954          }
955
956          if (!isShutdownInitiated())
957          {
958            synchronized (this)
959            {
960              if (!isShutdownInitiated())
961              {
962                final long sleepTime = computeSleepTimeUntilNextPurge(oldestNotPurgedCSN);
963                if (logger.isTraceEnabled())
964                {
965                  tracePurgeDetails(purgeCSN, oldestNotPurgedCSN, sleepTime);
966                  canDisplayNothingToPurgeMsg = true;
967                }
968                wait(sleepTime);
969              }
970            }
971          }
972        }
973        catch (InterruptedException e)
974        {
975          // shutdown initiated?
976        }
977        catch (Exception e)
978        {
979          logger.error(ERR_EXCEPTION_CHANGELOG_TRIM_FLUSH, stackTraceToSingleLineString(e));
980          if (replicationServer != null)
981          {
982            replicationServer.shutdown();
983          }
984        }
985      }
986    }
987
988    private void tracePurgeDetails(final CSN purgeCSN, final CSN oldestNotPurgedCSN, final long sleepTime)
989    {
990      if (purgeCSN.equals(oldestNotPurgedCSN.toStringUI()))
991      {
992        logger.trace("Purged up to %s. "
993            + "now sleeping until next purge during %s",
994            purgeCSN.toStringUI(), DurationUnit.toString(sleepTime));
995      }
996      else
997      {
998        logger.trace("Asked to purge up to %s, actually purged up to %s (not included). "
999            + "now sleeping until next purge during %s",
1000            purgeCSN.toStringUI(), oldestNotPurgedCSN.toStringUI(), DurationUnit.toString(sleepTime));
1001      }
1002    }
1003
1004    private long computeSleepTimeUntilNextPurge(CSN notPurgedCSN)
1005    {
1006      final long nextPurgeTime = notPurgedCSN.getTime();
1007      final long currentPurgeTime = TimeThread.getTime() - purgeDelayInMillis;
1008      if (currentPurgeTime < nextPurgeTime)
1009      {
1010        // sleep till the next CSN to purge,
1011        return nextPurgeTime - currentPurgeTime;
1012      }
1013      // wait a bit before purging more
1014      return DEFAULT_SLEEP;
1015    }
1016
1017    @Override
1018    public void initiateShutdown()
1019    {
1020      super.initiateShutdown();
1021      synchronized (this)
1022      {
1023        notify(); // wake up the purger thread for faster shutdown
1024      }
1025    }
1026  }
1027}