/* * 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 static org.opends.messages.ReplicationMessages.*; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import static org.opends.server.util.ServerConstants.EOL; import static org.opends.server.util.StaticUtils.getFileForPath; import static org.opends.server.util.StaticUtils.isLocalAddress; import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; import java.io.File; import java.io.IOException; import java.io.StringReader; import java.net.*; import java.util.*; import java.util.concurrent.CopyOnWriteArraySet; import org.opends.messages.Category; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import org.opends.messages.Severity; import org.opends.server.admin.server.ConfigurationChangeListener; import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn; import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn.*; import org.opends.server.admin.std.server.ReplicationServerCfg; import org.opends.server.admin.std.server.UserDefinedVirtualAttributeCfg; import org.opends.server.api.*; import org.opends.server.config.ConfigException; import org.opends.server.core.DirectoryServer; import org.opends.server.core.WorkflowImpl; import org.opends.server.core.networkgroups.NetworkGroup; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.common.*; import org.opends.server.replication.plugin.MultimasterReplication; import org.opends.server.replication.protocol.*; import org.opends.server.types.*; import org.opends.server.util.LDIFReader; import org.opends.server.util.ServerConstants; import org.opends.server.util.TimeThread; import org.opends.server.workflowelement.externalchangelog.ECLWorkflowElement; import com.sleepycat.je.DatabaseException; import org.opends.server.types.SearchScope; /** * ReplicationServer Listener. This singleton is the main object of the * replication server. It waits for the incoming connections and create listener * and publisher objects for connection with LDAP servers and with replication * servers It is responsible for creating the replication server * replicationServerDomain and managing it */ public final class ReplicationServer implements ConfigurationChangeListener<ReplicationServerCfg>, BackupTaskListener, RestoreTaskListener, ImportTaskListener, ExportTaskListener { private int serverId; private String serverURL; private ServerSocket listenSocket; private Thread listenThread; private Thread connectThread; /* The list of replication servers configured by the administrator */ private Collection<String> replicationServers; /* This table is used to store the list of dn for which we are currently * handling servers. */ private final Map<String, ReplicationServerDomain> baseDNs = new HashMap<String, ReplicationServerDomain>(); private volatile boolean shutdown = false; private ReplicationDbEnv dbEnv; private int rcvWindow; private int queueSize; private String dbDirname = null; // The delay (in sec) after which the changes must // be deleted from the persistent storage. private long purgeDelay; private int replicationPort; private boolean stopListen = false; private ReplSessionSecurity replSessionSecurity; // For the backend associated to this replication server, // DN of the config entry of the backend private DN backendConfigEntryDN; // ID of the backend private static final String backendId = "replicationChanges"; /* * Assured mode properties */ // Timeout (in milliseconds) when waiting for acknowledgments private long assuredTimeout = 1000; // Group id private byte groupId = (byte)1; // Number of pending changes for a DS, considered as threshold value to put // the DS in DEGRADED_STATUS. If value is 0, status analyzer is disabled private int degradedStatusThreshold = 5000; // Number of milliseconds to wait before sending new monitoring messages. // If value is 0, monitoring publisher is disabled private long monitoringPublisherPeriod = 3000; // The handler of the draft change numbers database, the database used to // store the relation between a draft change number ('seqnum') and the // associated cookie. // // Guarded by draftCNLock // private DraftCNDbHandler draftCNDbHandler; // The last value generated of the draft change number. // // Guarded by draftCNLock // private int lastGeneratedDraftCN = 0; // Used for protecting draft CN related state. private final Object draftCNLock = new Object(); /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); private static String externalChangeLogWorkflowID = "External Changelog Workflow ID"; private ECLWorkflowElement eclwe; private WorkflowImpl externalChangeLogWorkflowImpl = null; // This is required for unit testing, so that we can keep track of all the // replication servers which are running in the VM. private static Set<Integer> localPorts = new CopyOnWriteArraySet<Integer>(); // Monitors for synchronizing domain creation with the connect thread. private final Object domainTicketLock = new Object(); private final Object connectThreadLock = new Object(); private long domainTicket = 0L; // ServiceIDs excluded for ECL private ArrayList<String> excludedServiceIDs = new ArrayList<String>(); /** * The weight affected to the replication server. * Each replication server of the topology has a weight. When combined * together, the weights of the replication servers of a same group can be * translated to a percentage that determines the quantity of directory * servers of the topology that should be connected to a replication server. * For instance imagine a topology with 3 replication servers (with the same * group id) with the following weights: RS1=1, RS2=1, RS3=2. This means that * RS1 should have 25% of the directory servers connected in the topology, * RS2 25%, and RS3 50%. This may be useful if the replication servers of the * topology have a different power and one wants to spread the load between * the replication servers according to their power. */ private int weight = 1; /** * Holds the list of all replication servers instantiated in this VM. * This allows to perform clean up of the RS databases in unit tests. */ private static List<ReplicationServer> allInstances = new ArrayList<ReplicationServer>(); /** * Creates a new Replication server using the provided configuration entry. * * @param configuration The configuration of this replication server. * @throws ConfigException When Configuration is invalid. */ public ReplicationServer(ReplicationServerCfg configuration) throws ConfigException { replicationPort = configuration.getReplicationPort(); serverId = configuration.getReplicationServerId(); replicationServers = configuration.getReplicationServer(); if (replicationServers == null) replicationServers = new ArrayList<String>(); queueSize = configuration.getQueueSize(); purgeDelay = configuration.getReplicationPurgeDelay(); dbDirname = configuration.getReplicationDBDirectory(); rcvWindow = configuration.getWindowSize(); if (dbDirname == null) { dbDirname = "changelogDb"; } // Check that this path exists or create it. File f = getFileForPath(dbDirname); try { if (!f.exists()) { f.mkdir(); } } catch (Exception e) { MessageBuilder mb = new MessageBuilder(); mb.append(e.getLocalizedMessage()); mb.append(" "); mb.append(String.valueOf(getFileForPath(dbDirname))); Message msg = ERR_FILE_CHECK_CREATE_FAILED.get(mb.toString()); throw new ConfigException(msg, e); } groupId = (byte)configuration.getGroupId(); weight = configuration.getWeight(); assuredTimeout = configuration.getAssuredTimeout(); degradedStatusThreshold = configuration.getDegradedStatusThreshold(); monitoringPublisherPeriod = configuration.getMonitoringPeriod(); replSessionSecurity = new ReplSessionSecurity(); initialize(); configuration.addChangeListener(this); try { backendConfigEntryDN = DN.decode( "ds-cfg-backend-id=" + backendId + ",cn=Backends,cn=config"); } catch (Exception e) { /* do nothing */ } // Creates the backend associated to this ReplicationServer // if it does not exist. createBackend(); DirectoryServer.registerBackupTaskListener(this); DirectoryServer.registerRestoreTaskListener(this); DirectoryServer.registerExportTaskListener(this); DirectoryServer.registerImportTaskListener(this); localPorts.add(replicationPort); // Keep track of this new instance allInstances.add(this); } /** * Get the list of every replication servers instantiated in the current VM. * @return The list of every replication servers instantiated in the current * VM. */ public static List<ReplicationServer> getAllInstances() { return allInstances; } /** * The run method for the Listen thread. * This thread accept incoming connections on the replication server * ports from other replication servers or from LDAP servers * and spawn further thread responsible for handling those connections */ void runListen() { Message listenMsg = NOTE_REPLICATION_SERVER_LISTENING.get( getServerId(), listenSocket.getInetAddress().getHostAddress(), listenSocket.getLocalPort()); logError(listenMsg); while (!shutdown && !stopListen) { // Wait on the replicationServer port. // Read incoming messages and create LDAP or ReplicationServer listener // and Publisher. try { Session session; Socket newSocket = null; try { newSocket = listenSocket.accept(); newSocket.setTcpNoDelay(true); newSocket.setKeepAlive(true); int timeoutMS = MultimasterReplication.getConnectionTimeoutMS(); session = replSessionSecurity.createServerSession(newSocket, timeoutMS); if (session == null) // Error, go back to accept continue; } catch (Exception e) { // If problems happen during the SSL handshake, it is necessary // to close the socket to free the associated resources. if (newSocket != null) newSocket.close(); continue; } ReplicationMsg msg = session.receive(); if (msg instanceof ServerStartMsg) { DataServerHandler handler = new DataServerHandler(session, queueSize,serverURL,serverId,this,rcvWindow); handler.startFromRemoteDS((ServerStartMsg)msg); } else if (msg instanceof ReplServerStartMsg) { ReplicationServerHandler handler = new ReplicationServerHandler( session,queueSize,serverURL,serverId,this,rcvWindow); handler.startFromRemoteRS((ReplServerStartMsg)msg); } else if (msg instanceof ServerStartECLMsg) { ECLServerHandler handler = new ECLServerHandler( session,queueSize,serverURL,serverId,this,rcvWindow); handler.startFromRemoteServer((ServerStartECLMsg)msg); } else { // We did not recognize the message, close session as what // can happen after is undetermined and we do not want the server to // be disturbed ServerHandler.closeSession(session, null, null); return; } } catch (Exception e) { // The socket has probably been closed as part of the // shutdown or changing the port number process. // Just log debug information and loop. // Do not log the message during shutdown. if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } if (!shutdown) { Message message = ERR_EXCEPTION_LISTENING.get(e.getLocalizedMessage()); logError(message); } } } } /** * This method manages the connection with the other replication servers. * It periodically checks that this replication server is indeed connected * to all the other replication servers and if not attempts to * make the connection. */ void runConnect() { synchronized (connectThreadLock) { while (!shutdown) { /* * Periodically check that we are connected to all other replication * servers and if not establish the connection */ for (ReplicationServerDomain domain : getReplicationServerDomains()) { // Create a normalized set of server URLs. final Set<String> connectedReplServers = new HashSet<String>(); for (String url : domain.getChangelogs()) { connectedReplServers.add(normalizeServerURL(url)); } /* * check that all replication server in the config are in the * connected Set. If not create the connection */ for (String aServerURL : replicationServers) { final int separator = aServerURL.lastIndexOf(':'); final String portString = aServerURL.substring(separator + 1); final int port = Integer.parseInt(portString); final String hostname = aServerURL.substring(0, separator); final InetAddress inetAddress; try { inetAddress = InetAddress.getByName(hostname); } catch (UnknownHostException e) { // If the host name cannot be resolved then no chance of // connecting anyway. Message message = ERR_COULD_NOT_SOLVE_HOSTNAME.get(hostname); logError(message); continue; } // Avoid connecting to self. // FIXME: this will need changing if we ever support listening on // specific addresses. if (isLocalAddress(inetAddress) && (port == replicationPort)) { continue; } // Don't connect to a server if it is already connected. final String normalizedServerURL = normalizeServerURL(aServerURL); if (connectedReplServers.contains(normalizedServerURL)) { continue; } connect(aServerURL, domain.getBaseDn()); } } // Notify any threads waiting with domain tickets after each iteration. synchronized (domainTicketLock) { domainTicket++; domainTicketLock.notifyAll(); } // Retry each second. final int randomizer = (int) (Math.random() * 100); try { // Releases lock, allows threads to get domain ticket. connectThreadLock.wait(1000 + randomizer); } catch (InterruptedException e) { // Signalled to shutdown. return; } } } } /** * Establish a connection to the server with the address and port. * * @param remoteServerURL The address and port for the server, separated by a * colon. * @param baseDn The baseDn of the connection */ private void connect(String remoteServerURL, String baseDn) { int separator = remoteServerURL.lastIndexOf(':'); String port = remoteServerURL.substring(separator + 1); String hostname = remoteServerURL.substring(0, separator); boolean sslEncryption =replSessionSecurity.isSslEncryption(remoteServerURL); if (debugEnabled()) TRACER.debugInfo("RS " + this.getMonitorInstanceName() + " connects to " + remoteServerURL); Socket socket = new Socket(); Session session = null; try { InetSocketAddress ServerAddr = new InetSocketAddress( InetAddress.getByName(hostname), Integer.parseInt(port)); socket.setTcpNoDelay(true); int timeoutMS = MultimasterReplication.getConnectionTimeoutMS(); socket.connect(ServerAddr, timeoutMS); session = replSessionSecurity.createClientSession(socket, timeoutMS); ReplicationServerHandler handler = new ReplicationServerHandler( session, queueSize, this.serverURL, serverId, this, rcvWindow); handler.connect(baseDn, sslEncryption); } catch (Exception e) { if (session != null) { session.close(); } try { socket.close(); } catch (IOException ignored) { // Ignore. } } } /** * initialization function for the replicationServer. */ private void initialize() { shutdown = false; try { /* * Initialize the replicationServer database. */ dbEnv = new ReplicationDbEnv(getFileForPath(dbDirname).getAbsolutePath(), this); setServerURL(); listenSocket = new ServerSocket(); listenSocket.bind(new InetSocketAddress(replicationPort)); /* * creates working threads * We must first connect, then start to listen. */ if (debugEnabled()) TRACER.debugInfo("RS " +getMonitorInstanceName()+ " creates connect thread"); connectThread = new ReplicationServerConnectThread(this); connectThread.start(); if (debugEnabled()) TRACER.debugInfo("RS " +getMonitorInstanceName()+ " creates listen thread"); listenThread = new ReplicationServerListenThread(this); listenThread.start(); // Creates the ECL workflow elem so that DS (LDAPReplicationDomain) // can know me and really enableECL. if (WorkflowImpl.getWorkflow(externalChangeLogWorkflowID) != null) { // Already done . Nothing to do return; } eclwe = new ECLWorkflowElement(this); if (debugEnabled()) TRACER.debugInfo("RS " +getMonitorInstanceName()+ " successfully initialized"); } catch (DatabaseException e) { Message message = ERR_COULD_NOT_INITIALIZE_DB.get( getFileForPath(dbDirname).getAbsolutePath()); logError(message); } catch (ReplicationDBException e) { Message message = ERR_COULD_NOT_READ_DB.get(dbDirname, e.getLocalizedMessage()); logError(message); } catch (UnknownHostException e) { Message message = ERR_UNKNOWN_HOSTNAME.get(); logError(message); } catch (IOException e) { Message message = ERR_COULD_NOT_BIND_CHANGELOG.get(replicationPort, e.getMessage()); logError(message); } catch (DirectoryException e) { //FIXME:DirectoryException is raised by initializeECL => fix err msg Message message = Message.raw(Category.SYNC, Severity.SEVERE_ERROR, "Directory Exception raised by ECL initialization: " + e.getMessage()); logError(message); } } /** * Enable the ECL access by creating a dedicated workflow element. * @throws DirectoryException when an error occurs. */ public void enableECL() throws DirectoryException { if (externalChangeLogWorkflowImpl!=null) { // do nothing if ECL is already enabled return; } // Create the workflow for the base DN and register the workflow with // the server. externalChangeLogWorkflowImpl = new WorkflowImpl( externalChangeLogWorkflowID, DN.decode(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT), eclwe.getWorkflowElementID(), eclwe); externalChangeLogWorkflowImpl.register(); NetworkGroup defaultNetworkGroup = NetworkGroup.getDefaultNetworkGroup(); defaultNetworkGroup.registerWorkflow(externalChangeLogWorkflowImpl); // FIXME:ECL should the ECL Workflow be registered in adminNetworkGroup? NetworkGroup adminNetworkGroup = NetworkGroup.getAdminNetworkGroup(); adminNetworkGroup.registerWorkflow(externalChangeLogWorkflowImpl); // FIXME:ECL should the ECL Workflow be registered in internalNetworkGroup? NetworkGroup internalNetworkGroup = NetworkGroup.getInternalNetworkGroup(); internalNetworkGroup.registerWorkflow(externalChangeLogWorkflowImpl); enableECLVirtualAttr("lastexternalchangelogcookie", new LastCookieVirtualProvider()); enableECLVirtualAttr("firstchangenumber", new FirstChangeNumberVirtualAttributeProvider()); enableECLVirtualAttr("lastchangenumber", new LastChangeNumberVirtualAttributeProvider()); enableECLVirtualAttr("changelog", new ChangelogBaseDNVirtualAttributeProvider()); } private static void enableECLVirtualAttr(String attrName, VirtualAttributeProvider<UserDefinedVirtualAttributeCfg> provider) throws DirectoryException { Set<DN> baseDNs = new HashSet<DN>(0); Set<DN> groupDNs = new HashSet<DN>(0); Set<SearchFilter> filters = new HashSet<SearchFilter>(0); VirtualAttributeCfgDefn.ConflictBehavior conflictBehavior = ConflictBehavior.VIRTUAL_OVERRIDES_REAL; try { // To avoid the configuration in cn=config just // create a rule and register it into the DirectoryServer provider.initializeVirtualAttributeProvider(null); AttributeType attributeType = DirectoryServer.getAttributeType( attrName, false); SearchFilter filter = SearchFilter.createFilterFromString("objectclass=*"); filters.add(filter); baseDNs.add(DN.decode("")); VirtualAttributeRule rule = new VirtualAttributeRule(attributeType, provider, baseDNs, SearchScope.BASE_OBJECT, groupDNs, filters, conflictBehavior); DirectoryServer.registerVirtualAttribute(rule); } catch (Exception e) { Message message = NOTE_ERR_UNABLE_TO_ENABLE_ECL_VIRTUAL_ATTR.get(attrName, e.toString()); throw new DirectoryException(ResultCode.OPERATIONS_ERROR, message, e); } } private void shutdownECL() { WorkflowImpl eclwf = (WorkflowImpl) WorkflowImpl .getWorkflow(externalChangeLogWorkflowID); // do it only if not already done by another RS (unit test case) // if (DirectoryServer.getWorkflowElement(externalChangeLogWorkflowID) if (eclwf != null) { // FIXME:ECL should the ECL Workflow be registered in // internalNetworkGroup? NetworkGroup internalNetworkGroup = NetworkGroup .getInternalNetworkGroup(); internalNetworkGroup .deregisterWorkflow(externalChangeLogWorkflowID); // FIXME:ECL should the ECL Workflow be registered in adminNetworkGroup? NetworkGroup adminNetworkGroup = NetworkGroup .getAdminNetworkGroup(); adminNetworkGroup .deregisterWorkflow(externalChangeLogWorkflowID); NetworkGroup defaultNetworkGroup = NetworkGroup .getDefaultNetworkGroup(); defaultNetworkGroup .deregisterWorkflow(externalChangeLogWorkflowID); eclwf.deregister(); eclwf.finalizeWorkflow(); } eclwe = (ECLWorkflowElement) DirectoryServer .getWorkflowElement("EXTERNAL CHANGE LOG"); if (eclwe != null) { DirectoryServer.deregisterWorkflowElement(eclwe); eclwe.finalizeWorkflowElement(); } synchronized (draftCNLock) { if (draftCNDbHandler != null) { draftCNDbHandler.shutdown(); } } } /** * Get the ReplicationServerDomain associated to the base DN given in * parameter. * * @param baseDn The base Dn for which the ReplicationServerDomain must be * returned. * @param create Specifies whether to create the ReplicationServerDomain if * it does not already exist. * @return The ReplicationServerDomain associated to the base DN given in * parameter. */ public ReplicationServerDomain getReplicationServerDomain(String baseDn, boolean create) { return getReplicationServerDomain(baseDn, create, false); } /** * Get the ReplicationServerDomain associated to the base DN given in * parameter. * * @param baseDn The base Dn for which the ReplicationServerDomain must be * returned. * @param create Specifies whether to create the ReplicationServerDomain if * it does not already exist. * @param waitConnections Waits for the Connections with other RS to * be established before returning. * @return The ReplicationServerDomain associated to the base DN given in * parameter. */ public ReplicationServerDomain getReplicationServerDomain(String baseDn, boolean create, boolean waitConnections) { ReplicationServerDomain domain; synchronized (baseDNs) { domain = baseDNs.get(baseDn); if (domain != null ||!create) { return domain; } domain = new ReplicationServerDomain(baseDn, this); baseDNs.put(baseDn, domain); } if (waitConnections) { // Acquire a domain ticket and wait for a complete cycle of the connect // thread. final long myDomainTicket; synchronized (connectThreadLock) { // Connect thread must be waiting. synchronized (domainTicketLock) { // Determine the ticket which will be used in the next connect thread // iteration. myDomainTicket = domainTicket + 1; } // Wake up connect thread. connectThreadLock.notify(); } // Wait until the connect thread has processed next connect phase. synchronized (domainTicketLock) { // Condition. while (myDomainTicket > domainTicket && !shutdown) { try { // Wait with timeout so that we detect shutdown. domainTicketLock.wait(500); } catch (InterruptedException e) { // Can't do anything with this. Thread.currentThread().interrupt(); } } } } return domain; } /** * Shutdown the Replication Server service and all its connections. */ public void shutdown() { localPorts.remove(replicationPort); if (shutdown) return; shutdown = true; // shutdown the connect thread if (connectThread != null) { connectThread.interrupt(); } // shutdown the listener thread try { if (listenSocket != null) { listenSocket.close(); } } catch (IOException e) { // replication Server service is closing anyway. } // shutdown the listen thread if (listenThread != null) { listenThread.interrupt(); } // shutdown all the ChangelogCaches for (ReplicationServerDomain domain : getReplicationServerDomains()) { domain.shutdown(); } shutdownECL(); if (dbEnv != null) { dbEnv.shutdown(); } // Remove this instance from the global instance list allInstances.remove(this); } /** * Creates a new DB handler for this ReplicationServer and the serverId and * DN given in parameter. * * @param id The serverId for which the dbHandler must be created. * @param baseDn The DN for which the dbHandler must be created. * @return The new DB handler for this ReplicationServer and the serverId and * DN given in parameter. * @throws DatabaseException in case of underlying database problem. */ public DbHandler newDbHandler(int id, String baseDn) throws DatabaseException { return new DbHandler(id, baseDn, this, dbEnv, queueSize); } /** * Clears the generationId for the replicationServerDomain related to the * provided baseDn. * * @param baseDn * The baseDn for which to delete the generationId. */ public void clearGenerationId(String baseDn) { try { dbEnv.clearGenerationId(baseDn); } catch (Exception e) { // Ignore. if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.WARNING, e); } } synchronized (draftCNLock) { if (draftCNDbHandler != null) { try { draftCNDbHandler.clear(baseDn); } catch (Exception e) { // Ignore. if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.WARNING, e); } } try { lastGeneratedDraftCN = draftCNDbHandler.getLastKey(); } catch (Exception e) { // Ignore. if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.WARNING, e); } } } } } /** * Retrieves the time after which changes must be deleted from the * persistent storage (in milliseconds). * * @return The time after which changes must be deleted from the * persistent storage (in milliseconds). */ long getTrimAge() { return purgeDelay * 1000; } /** * Check if the provided configuration is acceptable for add. * * @param configuration The configuration to check. * @param unacceptableReasons When the configuration is not acceptable, this * table is use to return the reasons why this * configuration is not acceptable. * * @return true if the configuration is acceptable, false other wise. */ public static boolean isConfigurationAcceptable( ReplicationServerCfg configuration, List<Message> unacceptableReasons) { int port = configuration.getReplicationPort(); try { ServerSocket tmpSocket = new ServerSocket(); tmpSocket.bind(new InetSocketAddress(port)); tmpSocket.close(); } catch (Exception e) { Message message = ERR_COULD_NOT_BIND_CHANGELOG.get(port, e.getMessage()); unacceptableReasons.add(message); return false; } return true; } /** * {@inheritDoc} */ public ConfigChangeResult applyConfigurationChange( ReplicationServerCfg configuration) { // Some of those properties change don't need specific code. // They will be applied for next connections. Some others have immediate // effect disconnectRemovedReplicationServers(configuration.getReplicationServer()); replicationServers = configuration.getReplicationServer(); if (replicationServers == null) replicationServers = new ArrayList<String>(); queueSize = configuration.getQueueSize(); long newPurgeDelay = configuration.getReplicationPurgeDelay(); if (newPurgeDelay != purgeDelay) { purgeDelay = newPurgeDelay; // propagate for (ReplicationServerDomain domain : getReplicationServerDomains()) { domain.setPurgeDelay(purgeDelay*1000); } } rcvWindow = configuration.getWindowSize(); assuredTimeout = configuration.getAssuredTimeout(); // changing the listen port requires to stop the listen thread // and restart it. int newPort = configuration.getReplicationPort(); if (newPort != replicationPort) { stopListen = true; try { listenSocket.close(); listenThread.join(); stopListen = false; replicationPort = newPort; setServerURL(); listenSocket = new ServerSocket(); listenSocket.bind(new InetSocketAddress(replicationPort)); listenThread = new ReplicationServerListenThread(this); listenThread.start(); } catch (IOException e) { Message message = ERR_COULD_NOT_CLOSE_THE_SOCKET.get(e.toString()); logError(message); new ConfigChangeResult(DirectoryServer.getServerErrorResultCode(), false); } catch (InterruptedException e) { Message message = ERR_COULD_NOT_STOP_LISTEN_THREAD.get(e.toString()); logError(message); new ConfigChangeResult(DirectoryServer.getServerErrorResultCode(), false); } } // Update threshold value for status analyzers (stop them if requested // value is 0) if (degradedStatusThreshold != configuration .getDegradedStatusThreshold()) { int oldThresholdValue = degradedStatusThreshold; degradedStatusThreshold = configuration .getDegradedStatusThreshold(); for (ReplicationServerDomain domain : getReplicationServerDomains()) { if (degradedStatusThreshold == 0) { // Requested to stop analyzers domain.stopStatusAnalyzer(); } else if (domain.isRunningStatusAnalyzer()) { // Update the threshold value for this running analyzer domain.updateStatusAnalyzer(degradedStatusThreshold); } else if (oldThresholdValue == 0) { // Requested to start analyzers with provided threshold value if (domain.getConnectedDSs().size() > 0) domain.startStatusAnalyzer(); } } } // Update period value for monitoring publishers (stop them if requested // value is 0) if (monitoringPublisherPeriod != configuration .getMonitoringPeriod()) { long oldMonitoringPeriod = monitoringPublisherPeriod; monitoringPublisherPeriod = configuration.getMonitoringPeriod(); for (ReplicationServerDomain domain : getReplicationServerDomains()) { if (monitoringPublisherPeriod == 0L) { // Requested to stop monitoring publishers domain.stopMonitoringPublisher(); } else if (domain.isRunningMonitoringPublisher()) { // Update the threshold value for this running monitoring publisher domain.updateMonitoringPublisher(monitoringPublisherPeriod); } else if (oldMonitoringPeriod == 0L) { // Requested to start monitoring publishers with provided period value if ((domain.getConnectedDSs().size() > 0) || (domain.getConnectedRSs().size() > 0)) domain.startMonitoringPublisher(); } } } // Changed the group id ? byte newGroupId = (byte) configuration.getGroupId(); if (newGroupId != groupId) { groupId = newGroupId; // Have a new group id: Disconnect every servers. for (ReplicationServerDomain domain : getReplicationServerDomains()) { domain.stopAllServers(true); } } // Set a potential new weight if (weight != configuration.getWeight()) { weight = configuration.getWeight(); // Broadcast the new weight the the whole topology. This will make some // DSs reconnect (if needed) to other RSs according to the new weight of // this RS. broadcastConfigChange(); } if ((configuration.getReplicationDBDirectory() != null) && (!dbDirname.equals(configuration.getReplicationDBDirectory()))) { return new ConfigChangeResult(ResultCode.SUCCESS, true); } return new ConfigChangeResult(ResultCode.SUCCESS, false); } /* * Try and set a sensible URL for this replication server. Since we are * listening on all addresses there are a couple of potential candidates: 1) a * matching server url in the replication server's configuration, 2) hostname * local address. */ private void setServerURL() throws UnknownHostException { /* * First try the set of configured replication servers to see if one of them * is this replication server (this should always be the case). */ for (String rs : replicationServers) { /* * No need validate the string format because the admin framework has * already done it. */ final int index = rs.lastIndexOf(":"); final String hostname = rs.substring(0, index); final int port = Integer.parseInt(rs.substring(index + 1)); if (port == replicationPort && isLocalAddress(hostname)) { serverURL = rs; return; } } /* * Fall-back to the machine hostname. */ serverURL = InetAddress.getLocalHost().getHostName() + ":" + replicationPort; } /** * Broadcast a configuration change that just happened to the whole topology * by sending a TopologyMsg to every entity in the topology. */ private void broadcastConfigChange() { for (ReplicationServerDomain domain : getReplicationServerDomains()) { domain.buildAndSendTopoInfoToDSs(null); domain.buildAndSendTopoInfoToRSs(); } } /** * {@inheritDoc} */ public boolean isConfigurationChangeAcceptable( ReplicationServerCfg configuration, List<Message> unacceptableReasons) { return true; } /** * Get the value of generationId for the replication replicationServerDomain * associated with the provided baseDN. * * @param baseDN The baseDN of the replicationServerDomain. * @return The value of the generationID. */ public long getGenerationId(String baseDN) { ReplicationServerDomain rsd = this.getReplicationServerDomain(baseDN, false); if (rsd!=null) return rsd.getGenerationId(); return -1; } /** * Get the serverId for this replication server. * * @return The value of the serverId. * */ public int getServerId() { return serverId; } /** * Get the queueSize for this replication server. * * @return The maximum size of the queues for this Replication Server * */ public int getQueueSize() { return queueSize; } /** * Creates the backend associated to this replication server. * @throws ConfigException */ private void createBackend() throws ConfigException { try { String ldif = makeLdif( "dn: ds-cfg-backend-id="+backendId+",cn=Backends,cn=config", "objectClass: top", "objectClass: ds-cfg-backend", "ds-cfg-base-dn: dc="+backendId, "ds-cfg-enabled: true", "ds-cfg-writability-mode: enabled", "ds-cfg-java-class: " + "org.opends.server.replication.server.ReplicationBackend", "ds-cfg-backend-id: " + backendId); LDIFImportConfig ldifImportConfig = new LDIFImportConfig( new StringReader(ldif)); LDIFReader reader = new LDIFReader(ldifImportConfig); Entry backendConfigEntry = reader.readEntry(); if (!DirectoryServer.getConfigHandler().entryExists(backendConfigEntryDN)) { // Add the replication backend DirectoryServer.getConfigHandler().addEntry(backendConfigEntry, null); } ldifImportConfig.close(); } catch(Exception e) { MessageBuilder mb = new MessageBuilder(); mb.append(e.getLocalizedMessage()); Message msg = ERR_CHECK_CREATE_REPL_BACKEND_FAILED.get(mb.toString()); throw new ConfigException(msg, e); } } private static String makeLdif(String... lines) { StringBuilder buffer = new StringBuilder(); for (String line : lines) { buffer.append(line).append(EOL); } // Append an extra line so we can append LDIF Strings. buffer.append(EOL); return buffer.toString(); } /** * Do what needed when the config object related to this replication server * is deleted from the server configuration. */ public void remove() { if (debugEnabled()) TRACER.debugInfo("RS " +getMonitorInstanceName()+ " starts removing"); shutdown(); removeBackend(); DirectoryServer.deregisterBackupTaskListener(this); DirectoryServer.deregisterRestoreTaskListener(this); DirectoryServer.deregisterExportTaskListener(this); DirectoryServer.deregisterImportTaskListener(this); } /** * Removes the backend associated to this Replication Server that has been * created when this replication server was created. */ protected void removeBackend() { try { if (DirectoryServer.getConfigHandler().entryExists(backendConfigEntryDN)) { // Delete the replication backend DirectoryServer.getConfigHandler().deleteEntry(backendConfigEntryDN, null); } } catch(Exception e) { MessageBuilder mb = new MessageBuilder(); mb.append(e.getLocalizedMessage()); Message msg = ERR_DELETE_REPL_BACKEND_FAILED.get(mb.toString()); logError(msg); } } /** * {@inheritDoc} */ public void processBackupBegin(Backend backend, BackupConfig config) { // Nothing is needed at the moment } /** * {@inheritDoc} */ public void processBackupEnd(Backend backend, BackupConfig config, boolean successful) { // Nothing is needed at the moment } /** * {@inheritDoc} */ public void processRestoreBegin(Backend backend, RestoreConfig config) { if (backend.getBackendID().equals(backendId)) shutdown(); } /** * {@inheritDoc} */ public void processRestoreEnd(Backend backend, RestoreConfig config, boolean successful) { if (backend.getBackendID().equals(backendId)) initialize(); } /** * {@inheritDoc} */ public void processImportBegin(Backend backend, LDIFImportConfig config) { // Nothing is needed at the moment } /** * {@inheritDoc} */ public void processImportEnd(Backend backend, LDIFImportConfig config, boolean successful) { // Nothing is needed at the moment } /** * {@inheritDoc} */ public void processExportBegin(Backend backend, LDIFExportConfig config) { if (debugEnabled()) TRACER.debugInfo("RS " +getMonitorInstanceName()+ " Export starts"); if (backend.getBackendID().equals(backendId)) { // Retrieves the backend related to this replicationServerDomain // backend = ReplicationBackend b = (ReplicationBackend)DirectoryServer.getBackend(backendId); b.setServer(this); } } /** * {@inheritDoc} */ public void processExportEnd(Backend backend, LDIFExportConfig config, boolean successful) { // Nothing is needed at the moment } /** * Returns an iterator on the list of replicationServerDomain. * Returns null if none. * @return the iterator. */ public Iterator<ReplicationServerDomain> getDomainIterator() { Collection<ReplicationServerDomain> domains = getReplicationServerDomains(); if (!domains.isEmpty()) { return domains.iterator(); } else { return null; } } /** * Clears the Db associated with that server. */ public void clearDb() { Iterator<ReplicationServerDomain> rcachei = getDomainIterator(); if (rcachei != null) { while (rcachei.hasNext()) { ReplicationServerDomain rsd = rcachei.next(); rsd.clearDbs(); } } synchronized (draftCNLock) { if (draftCNDbHandler != null) { try { draftCNDbHandler.clear(); } catch (Exception e) { // Ignore. if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.WARNING, e); } } try { draftCNDbHandler.shutdown(); } catch (Exception e) { // Ignore. if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.WARNING, e); } } lastGeneratedDraftCN = 0; draftCNDbHandler = null; } } } /** * Get the assured mode timeout. * @return The assured mode timeout. */ public long getAssuredTimeout() { return assuredTimeout; } /** * Get The replication server group id. * @return The replication server group id. */ public byte getGroupId() { return groupId; } /** * Get the threshold value for status analyzer. * @return The threshold value for status analyzer. */ public int getDegradedStatusThreshold() { return degradedStatusThreshold; } /** * Get the monitoring publisher period value. * @return the monitoring publisher period value. */ public long getMonitoringPublisherPeriod() { return monitoringPublisherPeriod; } /** * Compute the list of replication servers that are not any * more connected to this Replication Server and stop the * corresponding handlers. * @param newReplServers the list of the new replication servers configured. */ private void disconnectRemovedReplicationServers( Collection<String> newReplServers) { Collection<String> serversToDisconnect = new ArrayList<String>(); for (String server: replicationServers) { if (!newReplServers.contains(server)) { try { // translate the server name into IP address // and keep the port number String[] host = server.split(":"); serversToDisconnect.add( (InetAddress.getByName(host[0])).getHostAddress() + ":" + host[1]); } catch (IOException e) { Message message = ERR_COULD_NOT_SOLVE_HOSTNAME.get(server); logError(message); } } } if (serversToDisconnect.isEmpty()) return; for (ReplicationServerDomain domain: getReplicationServerDomains()) { domain.stopReplicationServers(serversToDisconnect); } } /** * Retrieves a printable name for this Replication Server Instance. * * @return A printable name for this Replication Server Instance. */ public String getMonitorInstanceName() { return "Replication Server " + replicationPort + " " + serverId; } /** * Retrieves the port used by this ReplicationServer. * * @return The port used by this ReplicationServer. */ public int getReplicationPort() { return replicationPort; } /** * Create a new session to get the ECL. * @param msg The message that specifies the ECL request. * @return Returns the created session. * @throws DirectoryException When an error occurs. */ public ExternalChangeLogSession createECLSession(StartECLSessionMsg msg) throws DirectoryException { return new ExternalChangeLogSessionImpl(this, msg); } /** * Getter on the server URL. * @return the server URL. */ public String getServerURL() { return this.serverURL; } /** * WARNING : only use this methods for tests purpose. * * Add the Replication Server given as a parameter in the list * of local replication servers. * * @param server The server to be added. */ public static void onlyForTestsAddlocalReplicationServer(String server) { int separator = server.lastIndexOf(':'); if (separator == -1) return ; int port = Integer.parseInt(server.substring(separator + 1)); localPorts.add(port); } /** * WARNING : only use this methods for tests purpose. * * Clear the list of local Replication Servers * */ public static void onlyForTestsClearLocalReplicationServerList() { localPorts.clear(); } /** * Returns {@code true} if the provided port is one of the ports that this * replication server is listening on. * * @param port * The port to be checked. * @return {@code true} if the provided port is one of the ports that this * replication server is listening on. */ public static boolean isLocalReplicationServerPort(int port) { return localPorts.contains(port); } /** * Excluded a list of domain from eligibility computation. * @param excludedServiceIDs the provided list of serviceIDs excluded from * the computation of eligibleCN. */ public void disableEligibility(ArrayList<String> excludedServiceIDs) { this.excludedServiceIDs = excludedServiceIDs; } /** * Returns the eligible CN cross domains - relies on the eligible CN from * each domain. * @return the cross domain eligible CN. */ public ChangeNumber getEligibleCN() { String debugLog = ""; // traverse the domains and get the eligible CN from each domain // store the oldest one as the cross domain eligible CN ChangeNumber eligibleCN = null; Iterator<ReplicationServerDomain> rsdi = this.getDomainIterator(); if (rsdi != null) { while (rsdi.hasNext()) { ReplicationServerDomain domain = rsdi.next(); if ((excludedServiceIDs != null) && excludedServiceIDs.contains(domain.getBaseDn())) continue; ChangeNumber domainEligibleCN = domain.getEligibleCN(); String dates = ""; if (domainEligibleCN != null) { if ((eligibleCN == null) || (domainEligibleCN.older(eligibleCN))) { eligibleCN = domainEligibleCN; } dates = new Date(domainEligibleCN.getTime()).toString(); } debugLog += "[dn=" + domain.getBaseDn() + "] [eligibleCN=" + domainEligibleCN + ", " + dates + "]"; } } if (eligibleCN==null) { eligibleCN = new ChangeNumber(TimeThread.getTime(), 0, 0); } if (debugEnabled()) TRACER.debugInfo("In " + this + " getEligibleCN() ends with " + " the following domainEligibleCN for each domain :" + debugLog + " thus CrossDomainEligibleCN=" + eligibleCN + " ts=" + new Date(eligibleCN.getTime()).toString()); return eligibleCN; } /** * Get or create a handler on a Db on DraftCN for external changelog. * * @return the handler. * @throws DirectoryException * when needed. */ public DraftCNDbHandler getDraftCNDbHandler() throws DirectoryException { synchronized (draftCNLock) { try { if (draftCNDbHandler == null) { draftCNDbHandler = new DraftCNDbHandler(this, this.dbEnv); lastGeneratedDraftCN = getLastDraftChangeNumber(); } return draftCNDbHandler; } catch (Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); MessageBuilder mb = new MessageBuilder(); mb.append(ERR_DRAFT_CHANGENUMBER_DATABASE.get("")); throw new DirectoryException(ResultCode.OPERATIONS_ERROR, mb.toMessage(), e); } } } /** * Get the value of the first draft change number, 0 when db is empty. * @return the first value. */ public int getFirstDraftChangeNumber() { synchronized (draftCNLock) { if (draftCNDbHandler != null) { return draftCNDbHandler.getFirstKey(); } else { return 0; } } } /** * Get the value of the last draft change number, 0 when db is empty. * @return the last value. */ public int getLastDraftChangeNumber() { synchronized (draftCNLock) { if (draftCNDbHandler != null) { return draftCNDbHandler.getLastKey(); } else { return 0; } } } /** * Generate a new Draft ChangeNumber. * @return The generated Draft ChangeNUmber */ public int getNewDraftCN() { synchronized (draftCNLock) { return ++lastGeneratedDraftCN; } } /** * Get first and last DraftCN. * * @param crossDomainEligibleCN The provided crossDomainEligibleCN used as * the upper limit for the lastDraftCN * @param excludedServiceIDs The serviceIDs that are excluded from the ECL. * @return The first and last draftCN. * @throws DirectoryException When it happens. */ public int[] getECLDraftCNLimits( ChangeNumber crossDomainEligibleCN, ArrayList<String> excludedServiceIDs) throws DirectoryException { /* The content of the DraftCNdb depends on the SEARCH operations done before * requesting the DraftCN. If no operations, DraftCNdb is empty. * The limits we want to get are the "potential" limits if a request was * done, the DraftCNdb is probably not complete to do that. * * The first DraftCN is : * - the first record from the DraftCNdb * - if none because DraftCNdb empty, * then * if no change in replchangelog then return 0 * else return 1 (DraftCN that WILL be returned to next search) * * The last DraftCN is : * - initialized with the last record from the DraftCNdb (0 if none) * and consider the genState associated * - to the last DraftCN, we add the count of updates in the replchangelog * FROM that genState TO the crossDomainEligibleCN * (this diff is done domain by domain) */ int firstDraftCN; int lastDraftCN; Boolean dbEmpty = false; Long newestDate = 0L; DraftCNDbHandler draftCNDbH = this.getDraftCNDbHandler(); // Get the first DraftCN from the DraftCNdb firstDraftCN = draftCNDbH.getFirstKey(); Map<String,ServerState> domainsServerStateForLastSeqnum = null; ChangeNumber changeNumberForLastSeqnum = null; String domainForLastSeqnum = null; if (firstDraftCN < 1) { dbEmpty = true; firstDraftCN = 0; lastDraftCN = 0; } else { // Get the last DraftCN from the DraftCNdb lastDraftCN = draftCNDbH.getLastKey(); // Get the generalized state associated with the current last DraftCN // and initializes from it the startStates table String lastSeqnumGenState = draftCNDbH.getValue(lastDraftCN); if ((lastSeqnumGenState != null) && (lastSeqnumGenState.length()>0)) { domainsServerStateForLastSeqnum = MultiDomainServerState. splitGenStateToServerStates(lastSeqnumGenState); } // Get the changeNumber associated with the current last DraftCN changeNumberForLastSeqnum = draftCNDbH.getChangeNumber(lastDraftCN); // Get the domain associated with the current last DraftCN domainForLastSeqnum = draftCNDbH.getServiceID(lastDraftCN); } // Domain by domain Iterator<ReplicationServerDomain> rsdi = this.getDomainIterator(); if (rsdi != null) { while (rsdi.hasNext()) { // process a domain ReplicationServerDomain rsd = rsdi.next(); if (excludedServiceIDs.contains(rsd.getBaseDn())) continue; // for this domain, have the state in the replchangelog // where the last DraftCN update is long ec; if (domainsServerStateForLastSeqnum == null) { // Count changes of this domain from the beginning of the changelog ChangeNumber trimCN = new ChangeNumber(rsd.getLatestDomainTrimDate(), 0,0); ec = rsd.getEligibleCount( rsd.getStartState().duplicateOnlyOlderThan(trimCN), crossDomainEligibleCN); } else { // There are records in the draftDB (so already returned to clients) // BUT // There is nothing related to this domain in the last draft record // (may be this domain was disabled when this record was returned). // In that case, are counted the changes from // the date of the most recent change from this last draft record if (newestDate == 0L) { newestDate = changeNumberForLastSeqnum.getTime(); } // And count changes of this domain from the date of the // lastseqnum record (that does not refer to this domain) ChangeNumber cnx = new ChangeNumber(newestDate, changeNumberForLastSeqnum.getSeqnum(), 0); ec = rsd.getEligibleCount(cnx, crossDomainEligibleCN); if (domainForLastSeqnum.equalsIgnoreCase(rsd.getBaseDn())) ec--; } // cumulates on domains lastDraftCN += ec; // DraftCN Db is empty and there are eligible updates in the replication // changelog then init first DraftCN if ((ec>0) && (firstDraftCN==0)) firstDraftCN = 1; } } if (dbEmpty) { // The database was empty, just keep increasing numbers since last time // we generated one DraftCN. firstDraftCN += lastGeneratedDraftCN; lastDraftCN += lastGeneratedDraftCN; } return new int[]{firstDraftCN, lastDraftCN}; } /** * Returns the last (newest) cookie value. * @param excludedServiceIDs The list of serviceIDs excluded from ECL. * @return the last cookie value. */ public MultiDomainServerState getLastECLCookie( ArrayList<String> excludedServiceIDs) { disableEligibility(excludedServiceIDs); MultiDomainServerState result = new MultiDomainServerState(); // Initialize start state for all running domains with empty state Iterator<ReplicationServerDomain> rsdk = this.getDomainIterator(); if (rsdk != null) { while (rsdk.hasNext()) { // process a domain ReplicationServerDomain rsd = rsdk.next(); if ((excludedServiceIDs!=null) && (excludedServiceIDs.contains(rsd.getBaseDn()))) continue; if (rsd.getDbServerState().isEmpty()) continue; result.update(rsd.getBaseDn(), rsd.getEligibleState( getEligibleCN())); } } return result; } /** * Gets the weight. * @return the weight */ public int getWeight() { return weight; } private Collection<ReplicationServerDomain> getReplicationServerDomains() { synchronized (baseDNs) { return new ArrayList<ReplicationServerDomain>(baseDNs.values()); } } /** * Shuts down replication when an unexpected database exception occurs. Note * that we do not expect lock timeouts or txn timeouts because the replication * databases are deadlock free, thus all operations should complete * eventually. * * @param e * The unexpected database exception. */ void handleUnexpectedDatabaseException(DatabaseException e) { MessageBuilder mb = new MessageBuilder(); mb.append(ERR_CHANGELOG_SHUTDOWN_DATABASE_ERROR.get()); mb.append(stackTraceToSingleLineString(e)); logError(mb.toMessage()); shutdown(); } /** * Get the replication server DB directory. * This is useful for tests to be able to do some cleanup. Might even be * useful for the server some day. * * @return the Database directory name */ public String getDbDirName() { return dbDirname; } private String normalizeServerURL(final String url) { final int separator = url.lastIndexOf(':'); final String portString = url.substring(separator + 1); final String hostname = url.substring(0, separator); try { final InetAddress inetAddress = InetAddress.getByName(hostname); if (isLocalAddress(inetAddress)) { // It doesn't matter whether we use an IP or hostname here. return InetAddress.getLocalHost().getHostAddress() + ":" + portString; } else { return inetAddress.getHostAddress() + ":" + portString; } } catch (UnknownHostException e) { // This should not happen, but if it does then just default to the // original URL. Message message = ERR_COULD_NOT_SOLVE_HOSTNAME.get(hostname); logError(message); return url; } } }