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}