/* * 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.replication.common.StatusMachine.*; import static org.opends.server.replication.protocol.ProtocolVersion.*; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.zip.DataFormatException; import org.opends.messages.Message; import org.opends.server.replication.common.AssuredMode; import org.opends.server.replication.common.DSInfo; import org.opends.server.replication.common.ServerState; import org.opends.server.replication.common.ServerStatus; import org.opends.server.replication.common.StatusMachine; import org.opends.server.replication.common.StatusMachineEvent; import org.opends.server.replication.protocol.*; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeBuilder; import org.opends.server.types.Attributes; import org.opends.server.types.DirectoryException; import org.opends.server.types.ResultCode; /** * This class defines a server handler, which handles all interaction with a * peer server (RS or DS). */ public class DataServerHandler extends ServerHandler { // Temporary generationId received in handshake/phase1, // and used after handshake/phase2 long tmpGenerationId; // Status of this DS (only used if this server handler represents a DS) private ServerStatus status = ServerStatus.INVALID_STATUS; // Referrals URLs this DS is exporting private List<String> refUrls = new ArrayList<String>(); // Assured replication enabled on DS or not private boolean assuredFlag = false; // DS assured mode (relevant if assured replication enabled) private AssuredMode assuredMode = AssuredMode.SAFE_DATA_MODE; // DS safe data level (relevant if assured mode is safe data) private byte safeDataLevel = (byte) -1; private Set<String> eclIncludes = new HashSet<String>(); private Set<String> eclIncludesForDeletes = new HashSet<String>(); /** * Creates a new data server handler. * @param session The session opened with the remote data server. * @param queueSize The queue size. * @param replicationServerURL The URL of the hosting RS. * @param replicationServerId The serverID of the hosting RS. * @param replicationServer The hosting RS. * @param rcvWindowSize The receiving window size. */ public DataServerHandler( Session session, int queueSize, String replicationServerURL, int replicationServerId, ReplicationServer replicationServer, int rcvWindowSize) { super(session, queueSize, replicationServerURL, replicationServerId, replicationServer, rcvWindowSize); } /** * Order the peer DS server to change his status or close the connection * according to the requested new generation id. * @param newGenId The new generation id to take into account * @throws IOException If IO error occurred. */ public void changeStatusForResetGenId(long newGenId) throws IOException { StatusMachineEvent event; if (newGenId == -1) { // The generation id is being made invalid, let's put the DS // into BAD_GEN_ID_STATUS event = StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT; } else { if (newGenId == generationId) { if (status == ServerStatus.BAD_GEN_ID_STATUS) { // This server has the good new reference generation id. // Close connection with him to force his reconnection: DS will // reconnect in NORMAL_STATUS or DEGRADED_STATUS. if (debugEnabled()) { TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer().getServerId() + ". Closing connection to DS " + getServerId() + " for baseDn " + getServiceId() + " to force reconnection as new local" + " generationId and remote one match and DS is in bad gen id: " + newGenId); } // Connection closure must not be done calling RSD.stopHandler() as it // would rewait the RSD lock that we already must have entering this // method. This would lead to a reentrant lock which we do not want. // So simply close the session, this will make the hang up appear // after the reader thread that took the RSD lock realeases it. if (session != null) { // V4 protocol introduces a StopMsg to properly close the // connection between servers if (getProtocolVersion() >= ProtocolVersion.REPLICATION_PROTOCOL_V4) { try { session.publish(new StopMsg()); } catch (IOException ioe) { // Anyway, going to close session, so nothing to do } } } // NOT_CONNECTED_STATUS is the last one in RS session life: handler // will soon disappear after this method call... status = ServerStatus.NOT_CONNECTED_STATUS; return; } else { if (debugEnabled()) { TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer().getServerId() + ". DS " + getServerId() + " for baseDn " + getServiceId() + " has already generation id " + newGenId + " so no ChangeStatusMsg sent to him."); } return; } } else { // This server has a bad generation id compared to new reference one, // let's put it into BAD_GEN_ID_STATUS event = StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT; } } if ((event == StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT) && (status == ServerStatus.FULL_UPDATE_STATUS)) { // Prevent useless error message (full update status cannot lead to bad // gen status) Message message = NOTE_BAD_GEN_ID_IN_FULL_UPDATE.get( Integer.toString(replicationServerDomain. getReplicationServer().getServerId()), getServiceId(), Integer.toString(serverId), Long.toString(generationId), Long.toString(newGenId)); logError(message); return; } ServerStatus newStatus = StatusMachine.computeNewStatus(status, event); if (newStatus == ServerStatus.INVALID_STATUS) { Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(getServiceId(), Integer.toString(serverId), status.toString(), event.toString()); logError(msg); return; } // Send message requesting to change the DS status ChangeStatusMsg csMsg = new ChangeStatusMsg(newStatus, ServerStatus.INVALID_STATUS); if (debugEnabled()) { TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer().getServerId() + " Sending change status for reset gen id to " + getServerId() + " for baseDn " + getServiceId() + ":\n" + csMsg); } session.publish(csMsg); status = newStatus; } /** * Change the status according to the event generated from the status * analyzer. * @param event The event to be used for new status computation * @return The new status of the DS * @throws IOException When raised by the underlying session */ public ServerStatus changeStatusFromStatusAnalyzer(StatusMachineEvent event) throws IOException { // Check state machine allows this new status (Sanity check) ServerStatus newStatus = StatusMachine.computeNewStatus(status, event); if (newStatus == ServerStatus.INVALID_STATUS) { Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(getServiceId(), Integer.toString(serverId), status.toString(), event.toString()); logError(msg); // Status analyzer must only change from NORMAL_STATUS to DEGRADED_STATUS // and vice versa. We may are being trying to change the status while for // instance another status has just been entered: e.g a full update has // just been engaged. In that case, just ignore attempt to change the // status return newStatus; } // Send message requesting to change the DS status ChangeStatusMsg csMsg = new ChangeStatusMsg(newStatus, ServerStatus.INVALID_STATUS); if (debugEnabled()) { TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer().getServerId() + " Sending change status from status analyzer to " + getServerId() + " for baseDn " + getServiceId() + ":\n" + csMsg); } session.publish(csMsg); status = newStatus; return newStatus; } private void createStatusAnalyzer() { if (!replicationServerDomain.isRunningStatusAnalyzer()) { replicationServerDomain.startStatusAnalyzer(); } } /** * Retrieves a set of attributes containing monitor data that should be * returned to the client if the corresponding monitor entry is requested. * * @return A set of attributes containing monitor data that should be * returned to the client if the corresponding monitor entry is * requested. */ @Override public ArrayList<Attribute> getMonitorData() { // Get the generic ones ArrayList<Attribute> attributes = super.getMonitorData(); // Add the specific DS ones attributes.add(Attributes.create("replica", serverURL)); attributes.add(Attributes.create("connected-to", this.replicationServerDomain.getReplicationServer() .getMonitorInstanceName())); MonitorData md = replicationServerDomain.getDomainMonitorData(); // Oldest missing update Long approxFirstMissingDate = md.getApproxFirstMissingDate(serverId); if ((approxFirstMissingDate != null) && (approxFirstMissingDate > 0)) { Date date = new Date(approxFirstMissingDate); attributes.add(Attributes.create( "approx-older-change-not-synchronized", date.toString())); attributes.add(Attributes.create( "approx-older-change-not-synchronized-millis", String .valueOf(approxFirstMissingDate))); } // Missing changes long missingChanges = md.getMissingChanges(serverId); attributes.add(Attributes.create("missing-changes", String .valueOf(missingChanges))); // Replication delay long delay = md.getApproxDelay(serverId); attributes.add(Attributes.create("approximate-delay", String .valueOf(delay))); /* get the Server State */ AttributeBuilder builder = new AttributeBuilder("server-state"); ServerState state = md.getLDAPServerState(serverId); if (state != null) { for (String str : state.toStringSet()) { builder.add(str); } attributes.add(builder.toAttribute()); } return attributes; } /** * {@inheritDoc} */ @Override public String getMonitorInstanceName() { return "Connected directory server DS(" + serverId + ") " + serverURL + ",cn=" + replicationServerDomain.getMonitorInstanceName(); } /** * Gets the status of the connected DS. * @return The status of the connected DS. */ public ServerStatus getStatus() { return status; } /** * {@inheritDoc} */ @Override public boolean isDataServer() { return true; } /** * Process message of a remote server changing his status. * @param csMsg The message containing the new status * @return The new server status of the DS */ public ServerStatus processNewStatus(ChangeStatusMsg csMsg) { // Get the status the DS just entered ServerStatus reqStatus = csMsg.getNewStatus(); // Translate new status to a state machine event StatusMachineEvent event = StatusMachineEvent.statusToEvent(reqStatus); if (event == StatusMachineEvent.INVALID_EVENT) { Message msg = ERR_RS_INVALID_NEW_STATUS.get(reqStatus.toString(), getServiceId(), Integer.toString(serverId)); logError(msg); return ServerStatus.INVALID_STATUS; } // Check state machine allows this new status ServerStatus newStatus = StatusMachine.computeNewStatus(status, event); if (newStatus == ServerStatus.INVALID_STATUS) { Message msg = ERR_RS_CANNOT_CHANGE_STATUS.get(getServiceId(), Integer.toString(serverId), status.toString(), event.toString()); logError(msg); return ServerStatus.INVALID_STATUS; } status = newStatus; return status; } /** * Processes a start message received from a remote data server. * @param serverStartMsg The provided start message received. * @return flag specifying whether the remote server requests encryption. * @throws DirectoryException raised when an error occurs. */ public boolean processStartFromRemote(ServerStartMsg serverStartMsg) throws DirectoryException { session .setProtocolVersion(getCompatibleVersion(serverStartMsg.getVersion())); tmpGenerationId = serverStartMsg.getGenerationId(); serverId = serverStartMsg.getServerId(); serverURL = serverStartMsg.getServerURL(); groupId = serverStartMsg.getGroupId(); heartbeatInterval = serverStartMsg.getHeartbeatInterval(); // generic stuff setServiceIdAndDomain(serverStartMsg.getBaseDn(), true); setInitialServerState(serverStartMsg.getServerState()); setSendWindowSize(serverStartMsg.getWindowSize()); if (heartbeatInterval < 0) { heartbeatInterval = 0; } return serverStartMsg.getSSLEncryption(); } /** * Registers this handler into its related domain and notifies the domain * about the new DS. */ public void registerIntoDomain() { // All-right, connected with new DS: store handler. Map<Integer, DataServerHandler> connectedDSs = replicationServerDomain.getConnectedDSs(); connectedDSs.put(serverId, this); // Tell peer DSs a new DS just connected to us // No need to re-send TopologyMsg to this just new DS so not null // argument replicationServerDomain.buildAndSendTopoInfoToDSs(this); // Tell peer RSs a new DS just connected to us replicationServerDomain.buildAndSendTopoInfoToRSs(); } // Send our own TopologyMsg to DS private TopologyMsg sendTopoToRemoteDS() throws IOException { TopologyMsg outTopoMsg = replicationServerDomain .createTopologyMsgForDS(this.serverId); sendTopoInfo(outTopoMsg); return outTopoMsg; } /** * Starts the handler from a remote ServerStart message received from * the remote data server. * @param inServerStartMsg The provided ServerStart message received. */ public void startFromRemoteDS(ServerStartMsg inServerStartMsg) { try { // initializations localGenerationId = -1; oldGenerationId = -100; // processes the ServerStart message received boolean sessionInitiatorSSLEncryption = processStartFromRemote(inServerStartMsg); /** * Hack to be sure that if a server disconnects and reconnect, we * let the reader thread see the closure and cleanup any reference * to old connection. This must be done before taking the domain lock so * that the reader thread has a chance to stop the handler. * * TODO: This hack should be removed and disconnection/reconnection * properly dealt with. */ if (replicationServerDomain.getConnectedDSs() .containsKey(inServerStartMsg.getServerId())) { try { Thread.sleep(100); } catch(Exception e){ abortStart(null); return; } } // lock with no timeout lockDomain(false); localGenerationId = replicationServerDomain.getGenerationId(); oldGenerationId = localGenerationId; // Duplicate server ? if (!replicationServerDomain.checkForDuplicateDS(this)) { abortStart(null); return; } try { StartMsg outStartMsg = sendStartToRemote(); // log logStartHandshakeRCVandSND(inServerStartMsg, outStartMsg); // The session initiator decides whether to use SSL. // Until here session is encrypted then it depends on the negotiation if (!sessionInitiatorSSLEncryption) session.stopEncryption(); // wait and process StartSessionMsg from remote RS StartSessionMsg inStartSessionMsg = waitAndProcessStartSessionFromRemoteDS(); if (inStartSessionMsg == null) { // DS wants to properly close the connection (DS sent a StopMsg) logStopReceived(); abortStart(null); return; } // Send our own TopologyMsg to remote DS TopologyMsg outTopoMsg = sendTopoToRemoteDS(); logStartSessionHandshake(inStartSessionMsg, outTopoMsg); } catch(IOException e) { Message errMessage = ERR_DS_DISCONNECTED_DURING_HANDSHAKE.get( Integer.toString(inServerStartMsg.getServerId()), Integer.toString(replicationServerDomain.getReplicationServer(). getServerId())); throw new DirectoryException(ResultCode.OTHER, errMessage); } catch (NotSupportedOldVersionPDUException e) { // We do not need to support DS V1 connection, we just accept RS V1 // connection: // We just trash the message, log the event for debug purpose and close // the connection throw new DirectoryException(ResultCode.OTHER, null, null); } catch (Exception e) { // We do not need to support DS V1 connection, we just accept RS V1 // connection: // We just trash the message, log the event for debug purpose and close // the connection throw new DirectoryException(ResultCode.OTHER, null, null); } // Create the status analyzer for the domain if not already started createStatusAnalyzer(); // Create the monitoring publisher for the domain if not already started createMonitoringPublisher(); registerIntoDomain(); Message message = INFO_REPLICATION_SERVER_CONNECTION_FROM_DS .get(getReplicationServerId(), getServerId(), replicationServerDomain.getBaseDn(), session.getReadableRemoteAddress()); logError(message); super.finalizeStart(); } catch(DirectoryException de) { abortStart(de.getMessageObject()); } catch(Exception e) { abortStart(null); } finally { if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); } } /** * Sends a start message to the remote DS. * * @return The StartMsg sent. * @throws IOException * When an exception occurs. */ private StartMsg sendStartToRemote() throws IOException { final StartMsg startMsg; // Before V4 protocol, we sent a ReplServerStartMsg if (getProtocolVersion() < ProtocolVersion.REPLICATION_PROTOCOL_V4) { // Peer DS uses protocol < V4 : send it a ReplServerStartMsg startMsg = new ReplServerStartMsg(replicationServerId, replicationServerURL, getServiceId(), maxRcvWindow, replicationServerDomain.getDbServerState(), localGenerationId, sslEncryption, getLocalGroupId(), replicationServerDomain.getReplicationServer() .getDegradedStatusThreshold()); } else { // Peer DS uses protocol V4 : send it a ReplServerStartDSMsg startMsg = new ReplServerStartDSMsg(replicationServerId, replicationServerURL, getServiceId(), maxRcvWindow, replicationServerDomain.getDbServerState(), localGenerationId, sslEncryption, getLocalGroupId(), replicationServerDomain.getReplicationServer() .getDegradedStatusThreshold(), replicationServer.getWeight(), replicationServerDomain.getConnectedLDAPservers().size()); } send(startMsg); return startMsg; } /** * Creates a DSInfo structure representing this remote DS. * @return The DSInfo structure representing this remote DS */ public DSInfo toDSInfo() { return new DSInfo(serverId, serverURL, replicationServerId, generationId, status, assuredFlag, assuredMode, safeDataLevel, groupId, refUrls, eclIncludes, eclIncludesForDeletes, getProtocolVersion()); } /** * {@inheritDoc} */ @Override public String toString() { if (serverId != 0) { StringBuilder builder = new StringBuilder("Replica DS("); builder.append(serverId); builder.append(") for domain \""); builder.append(replicationServerDomain.getBaseDn()); builder.append("\""); return builder.toString(); } else { return "Unknown server"; } } /** * Wait receiving the StartSessionMsg from the remote DS and process it, or * receiving a StopMsg to properly stop the handshake procedure. * @return the startSessionMsg received or null DS sent a stop message to * not finish the handshake. * @throws DirectoryException * @throws IOException * @throws ClassNotFoundException * @throws DataFormatException * @throws NotSupportedOldVersionPDUException */ private StartSessionMsg waitAndProcessStartSessionFromRemoteDS() throws DirectoryException, IOException, ClassNotFoundException, DataFormatException, NotSupportedOldVersionPDUException { ReplicationMsg msg = session.receive(); if (msg instanceof StopMsg) { // DS wants to stop handshake (was just for handshake phase one for RS // choice). Return null to make the session be terminated. return null; } else if (!(msg instanceof StartSessionMsg)) { Message message = Message.raw( "Protocol error: StartSessionMsg required." + msg + " received."); abortStart(message); return null; } // Process StartSessionMsg sent by remote DS StartSessionMsg startSessionMsg = (StartSessionMsg) msg; this.status = startSessionMsg.getStatus(); // Sanity check: is it a valid initial status? if (!isValidInitialStatus(this.status)) { Message message = ERR_RS_INVALID_INIT_STATUS.get( this.status.toString(), getServiceId(), Integer.toString(serverId)); throw new DirectoryException(ResultCode.OTHER, message); } this.refUrls = startSessionMsg.getReferralsURLs(); this.assuredFlag = startSessionMsg.isAssured(); this.assuredMode = startSessionMsg.getAssuredMode(); this.safeDataLevel = startSessionMsg.getSafeDataLevel(); this.eclIncludes = startSessionMsg.getEclIncludes(); this.eclIncludesForDeletes = startSessionMsg.getEclIncludesForDeletes(); /* * If we have already a generationID set for the domain * then * if the connecting replica has not the same * then it is degraded locally and notified by an error message * else * we set the generationID from the one received * (unsaved yet on disk . will be set with the 1rst change * received) */ generationId = tmpGenerationId; if (localGenerationId > 0) { if (generationId != localGenerationId) { Message message = WARN_BAD_GENERATION_ID_FROM_DS.get( serverId, session.getReadableRemoteAddress(), generationId, getServiceId(), getReplicationServerId(), localGenerationId); logError(message); } } else { // We are an empty Replicationserver if ((generationId > 0) && (!getServerState().isEmpty())) { // If the LDAP server has already sent changes // it is not expected to connect to an empty RS Message message = WARN_BAD_GENERATION_ID_FROM_DS.get( serverId, session.getReadableRemoteAddress(), generationId, getServiceId(), getReplicationServerId(), localGenerationId); logError(message); } else { // The local RS is not initialized - take the one received // WARNING: Must be done before computing topo message to send // to peer server as topo message must embed valid generation id // for our server oldGenerationId = replicationServerDomain.changeGenerationId(generationId, false); } } return startSessionMsg; } /** * Process message of a remote server changing his status. * @param csMsg The message containing the new status */ public void receiveNewStatus(ChangeStatusMsg csMsg) { if (replicationServerDomain!=null) replicationServerDomain.processNewStatus(this, csMsg); } }