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