/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at * trunk/opends/resource/legal-notices/OpenDS.LICENSE * or https://OpenDS.dev.java.net/OpenDS.LICENSE. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, * add the following below this CDDL HEADER, with the fields enclosed * by brackets "[]" replaced with your own identifying information: * Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END * * * Copyright 2006-2010 Sun Microsystems, Inc. * Portions copyright 2011-2013 ForgeRock AS */ package org.opends.server.replication.server; import org.opends.messages.MessageBuilder; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.messages.ReplicationMessages.*; import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.LinkedList; import org.opends.server.admin.std.server.MonitorProviderCfg; import org.opends.server.api.DirectoryThread; import org.opends.server.api.MonitorProvider; import org.opends.server.config.ConfigException; import org.opends.server.types.Attribute; import org.opends.server.types.Attributes; import org.opends.server.types.InitializationException; import org.opends.server.util.TimeThread; import org.opends.server.core.DirectoryServer; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.server.ReplicationDB.ReplServerDBCursor; import com.sleepycat.je.DatabaseException; /** * This class is used for managing the replicationServer database for each * server in the topology. * It is responsible for efficiently saving the updates that is received from * each master server into stable storage. * This class is also able to generate a ReplicationIterator that can be * used to read all changes from a given ChangeNUmber. * * This class publish some monitoring information below cn=monitor. * */ public class DbHandler implements Runnable { // The msgQueue holds all the updates not yet saved to stable storage. // This list is only used as a temporary placeholder so that the write // in the stable storage can be grouped for efficiency reason. // Adding an update synchronously add the update to this list. // A dedicated thread loops on flush() and trim(). // flush() : get a number of changes from the in memory list by block // and write them to the db. // trim() : deletes from the DB a number of changes that are older than a // certain date. // // Changes are not read back by replicationServer threads that are responsible // for pushing the changes to other replication server or to LDAP server // private final LinkedList<UpdateMsg> msgQueue = new LinkedList<UpdateMsg>(); // The High and low water mark for the max size of the msgQueue. // the threads calling add() method will be blocked if the size of // msgQueue becomes larger than the queueHimark and will resume // only when the size of the msgQueue goes below queueLowmark. int queueMaxSize = 5000; int queueLowmark = 1000; int queueHimark = 4000; // The queue himark and lowmark in bytes, this is set to 100 times the // himark and lowmark in number of updates. int queueMaxBytes = 100 * queueMaxSize; int queueLowmarkBytes = 100 * queueLowmark; int queueHimarkBytes = 100 * queueHimark; // The number of bytes currently in the queue int queueByteSize = 0; private ReplicationDB db; private ChangeNumber firstChange = null; private ChangeNumber lastChange = null; private int serverId; private String baseDn; private DbMonitorProvider dbMonitor = new DbMonitorProvider(); private boolean shutdown = false; private boolean done = false; private DirectoryThread thread; private final Object flushLock = new Object(); private ReplicationServer replicationServer; private long latestTrimDate = 0; /** * * The trim age in milliseconds. Changes record in the change DB that * are older than this age are removed. * */ private long trimAge; /** * Creates a new dbHandler associated to a given LDAP server. * * @param id Identifier of the DB. * @param baseDn the baseDn for which this DB was created. * @param replicationServer The ReplicationServer that creates this dbHandler. * @param dbenv the Database Env to use to create the ReplicationServer DB. * server for this domain. * @param queueSize The queueSize to use when creating the dbHandler. * @throws DatabaseException If a database problem happened */ public DbHandler( int id, String baseDn, ReplicationServer replicationServer, ReplicationDbEnv dbenv, int queueSize) throws DatabaseException { this.replicationServer = replicationServer; serverId = id; this.baseDn = baseDn; trimAge = replicationServer.getTrimAge(); queueMaxSize = queueSize; queueLowmark = queueSize / 5; queueHimark = queueSize * 4 / 5; queueMaxBytes = 200 * queueMaxSize; queueLowmarkBytes = 200 * queueLowmark; queueHimarkBytes = 200 * queueLowmark; db = new ReplicationDB(id, baseDn, replicationServer, dbenv); firstChange = db.readFirstChange(); lastChange = db.readLastChange(); thread = new DirectoryThread(this, "Replication server RS(" + replicationServer.getServerId() + ") changelog checkpointer for Replica DS(" + id + ") for domain \"" + baseDn + "\""); thread.start(); DirectoryServer.deregisterMonitorProvider(dbMonitor); DirectoryServer.registerMonitorProvider(dbMonitor); } /** * Add an update to the list of messages that must be saved to the db * managed by this db handler. * This method is blocking if the size of the list of message is larger * than its maximum. * * @param update The update that must be saved to the db managed by this db * handler. */ public void add(UpdateMsg update) { synchronized (msgQueue) { int size = msgQueue.size(); if ((size > queueHimark) || (queueByteSize > queueHimarkBytes)) msgQueue.notify(); while ((size > queueMaxSize) || (queueByteSize > queueMaxBytes)) { try { msgQueue.wait(500); } catch (InterruptedException e) { // simply loop to try again. } size = msgQueue.size(); } queueByteSize += update.size(); msgQueue.add(update); if (lastChange == null || lastChange.older(update.getChangeNumber())) { lastChange = update.getChangeNumber(); } if (firstChange == null) firstChange = update.getChangeNumber(); } } /** * Get some changes out of the message queue of the LDAP server. * (from the beginning of the queue) * @param number the maximum number of messages to extract. * @return a List containing number changes extracted from the queue. */ private List<UpdateMsg> getChanges(int number) { int current = 0; LinkedList<UpdateMsg> changes = new LinkedList<UpdateMsg>(); synchronized (msgQueue) { int size = msgQueue.size(); while ((current < number) && (current < size)) { UpdateMsg msg = msgQueue.get(current); current++; changes.add(msg); } } return changes; } /** * Get the firstChange. * @return Returns the firstChange. */ public ChangeNumber getFirstChange() { return firstChange; } /** * Get the lastChange. * @return Returns the lastChange. */ public ChangeNumber getLastChange() { return lastChange; } /** * Get the number of changes. * * @return Returns the number of changes. */ public long getChangesCount() { try { return lastChange.getSeqnum() - firstChange.getSeqnum() + 1; } catch (Exception e) { return 0; } } /** * Generate a new ReplicationIterator that allows to browse the db * managed by this dbHandler and starting at the position defined * by a given changeNumber. * * @param changeNumber The position where the iterator must start. * * @return a new ReplicationIterator that allows to browse the db * managed by this dbHandler and starting at the position defined * by a given changeNumber. * * @throws DatabaseException if a database problem happened. * @throws Exception If there is no other change to push after change * with changeNumber number. */ public ReplicationIterator generateIterator(ChangeNumber changeNumber) throws DatabaseException, Exception { if (changeNumber == null) { flush(); } return new ReplicationIterator(serverId, db, changeNumber, this); } /** * Removes the provided number of messages from the beginning of the msgQueue. * * @param number the number of changes to be removed. */ private void clearQueue(int number) { synchronized (msgQueue) { int current = 0; while ((current < number) && (!msgQueue.isEmpty())) { UpdateMsg msg = msgQueue.remove(); // remove first queueByteSize -= msg.size(); current++; } if ((msgQueue.size() < queueLowmark) && (queueByteSize < queueLowmarkBytes)) msgQueue.notifyAll(); } } /** * Shutdown this dbHandler. */ public void shutdown() { if (shutdown) { return; } shutdown = true; synchronized (msgQueue) { msgQueue.notifyAll(); } synchronized (this) { /* Can this be replaced with thread.join() ? */ while (!done) { try { this.wait(); } catch (Exception e) { /* do nothing */} } } while (msgQueue.size() != 0) flush(); db.shutdown(); DirectoryServer.deregisterMonitorProvider(dbMonitor); } /** * Run method for this class. * Periodically Flushes the ReplicationServerDomain cache from memory to the * stable storage and trims the old updates. */ public void run() { while (!shutdown) { try { flush(); trim(); synchronized (msgQueue) { if ((msgQueue.size() < queueLowmark) && (queueByteSize < queueLowmarkBytes)) { try { msgQueue.wait(1000); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } } } catch (Exception end) { MessageBuilder mb = new MessageBuilder(); mb.append(ERR_EXCEPTION_CHANGELOG_TRIM_FLUSH.get()); mb.append(stackTraceToSingleLineString(end)); logError(mb.toMessage()); synchronized (this) { // set the done variable to true so that this thread don't // get stuck in this dbHandler.shutdown() when it get called // by replicationServer.shutdown(); done = true; } if (replicationServer != null) replicationServer.shutdown(); break; } } // call flush a last time before exiting to make sure that // no change was forgotten in the msgQueue flush(); synchronized (this) { done = true; this.notifyAll(); } } /** * Retrieves the latest trim date. * @return the latest trim date. */ public long getLatestTrimDate() { return latestTrimDate; } /** * Trim old changes from this replicationServer database. * @throws DatabaseException In case of database problem. */ private void trim() throws DatabaseException, Exception { if (trimAge == 0) { return; } latestTrimDate = TimeThread.getTime() - trimAge; ChangeNumber trimDate = new ChangeNumber(latestTrimDate, 0, 0); // Find the last changeNumber before the trimDate, in the Database. ChangeNumber lastBeforeTrimDate = db .getPreviousChangeNumber(trimDate); if (lastBeforeTrimDate != null) { // If we found it, we want to stop trimming when reaching it. trimDate = lastBeforeTrimDate; } for (int i = 0; i < 100; i++) { synchronized (flushLock) { /* * the trim is done by group in order to save some CPU and IO bandwidth * start the transaction then do a bunch of remove then commit */ final ReplServerDBCursor cursor = db.openDeleteCursor(); try { for (int j = 0; j < 50; j++) { ChangeNumber changeNumber = cursor.nextChangeNumber(); if (changeNumber == null) { cursor.close(); done = true; return; } if ((!changeNumber.equals(lastChange)) && (changeNumber.older(trimDate))) { cursor.delete(); } else { firstChange = changeNumber; cursor.close(); done = true; return; } } cursor.close(); } catch (Exception e) { // mark shutdown for this db so that we don't try again to // stop it from cursor.close() or methods called by cursor.close() cursor.abort(); shutdown = true; throw e; } } } } /** * Flush a number of updates from the memory list to the stable storage. * Flush is done by chunk sized to 500 messages, starting from the * beginning of the list. */ public void flush() { int size; int chunksize = (500 < queueMaxSize ? 500 : queueMaxSize); do { synchronized(flushLock) { // get N (or less) messages from the queue to save to the DB // (from the beginning of the queue) List<UpdateMsg> changes = getChanges(chunksize); // if no more changes to save exit immediately. if ((changes == null) || ((size = changes.size()) == 0)) return; // save the change to the stable storage. db.addEntries(changes); // remove the changes from the list of changes to be saved // (remove from the beginning of the queue) clearQueue(changes.size()); } } while (size >= chunksize); } /** * This internal class is used to implement the Monitoring capabilities * of the dbHandler. */ private class DbMonitorProvider extends MonitorProvider<MonitorProviderCfg> { /** * {@inheritDoc} */ @Override public ArrayList<Attribute> getMonitorData() { ArrayList<Attribute> attributes = new ArrayList<Attribute>(); attributes.add(Attributes.create("replicationServer-database", String.valueOf(serverId))); attributes.add(Attributes.create("domain-name", baseDn)); if (firstChange != null) { Date firstTime = new Date(firstChange.getTime()); attributes.add(Attributes.create("first-change", firstChange .toString() + " " + firstTime.toString())); } if (lastChange != null) { Date lastTime = new Date(lastChange.getTime()); attributes.add(Attributes.create("last-change", lastChange .toString() + " " + lastTime.toString())); } attributes.add( Attributes.create("queue-size", String.valueOf(msgQueue.size()))); attributes.add( Attributes.create("queue-size-bytes", String.valueOf(queueByteSize))); return attributes; } /** * {@inheritDoc} */ @Override public String getMonitorInstanceName() { ReplicationServerDomain domain = replicationServer .getReplicationServerDomain(baseDn, false); return "Changelog for DS(" + serverId + "),cn=" + domain.getMonitorInstanceName(); } /** * {@inheritDoc} */ @Override public void initializeMonitorProvider(MonitorProviderCfg configuration) throws ConfigException,InitializationException { // Nothing to do for now } } /** * {@inheritDoc} */ @Override public String toString() { return(baseDn + " " + serverId + " " + firstChange + " " + lastChange); } /** * Set the Purge delay for this db Handler. * @param delay The purge delay in Milliseconds. */ public void setPurgeDelay(long delay) { trimAge = delay; } /** * Clear the changes from this DB (from both memory cache and DB storage). * @throws DatabaseException When an exception occurs while removing the * changes from the DB. * @throws Exception When an exception occurs while accessing a resource * from the DB. * */ public void clear() throws DatabaseException, Exception { synchronized(flushLock) { msgQueue.clear(); queueByteSize = 0; db.clear(); firstChange = db.readFirstChange(); lastChange = db.readLastChange(); } } /** * Getter fot the serverID of the server for which this database is managed. * * @return the serverId. */ public int getServerId() { return this.serverId; } /** * Return the size of the msgQueue (the memory cache of the DbHandler). * For test purpose. * @return The memory queue size. */ public int getQueueSize() { return this.msgQueue.size(); } /** * Set the counter writing window size (public for unit tests only). * @param size Size in number of record. */ public void setCounterWindowSize(int size) { db.setCounterWindowSize(size); } /** * Return the number of changes between 2 provided change numbers. * This a alternative to traverseAndCount, expected to be much more efficient * when there is a huge number of changes in the Db. * @param from The lower (older) change number. * @param to The upper (newer) change number. * @return The computed number of changes. */ public int getCount(ChangeNumber from, ChangeNumber to) { int c=0; // Now that we always keep the last ChangeNumber in the DB to avoid // expiring cookies to quickly, we need to check if the "to" // is older than the trim date. if ((to == null) || !to.older(new ChangeNumber(latestTrimDate, 0, 0))) { flush(); c = db.count(from, to); } return c; } }