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}