/* * 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 legal-notices/CDDLv1_0.txt * or http://forgerock.org/license/CDDLv1.0.html. * 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 legal-notices/CDDLv1_0.txt. * 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-2015 ForgeRock AS */ package org.opends.server.replication.server; import static org.opends.messages.ReplicationMessages.*; import static org.opends.server.replication.common.ServerStatus.*; import static org.opends.server.replication.common.StatusMachine.*; import static org.opends.server.replication.protocol.ProtocolVersion.*; import java.io.IOException; import java.util.*; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ResultCode; import org.opends.server.replication.common.*; import org.opends.server.replication.protocol.*; import org.opends.server.types.*; /** * This class defines a server handler, which handles all interaction with a * peer server (RS or DS). */ public class DataServerHandler extends ServerHandler { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * Temporary generationId received in handshake/phase1, and used after * handshake/phase2. */ private 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<>(); /** Assured replication enabled on DS or not. */ private boolean assuredFlag; /** 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 = -1; private Set<String> eclIncludes = new HashSet<>(); private Set<String> eclIncludesForDeletes = new HashSet<>(); /** * Creates a new data server handler. * @param session The session opened with the remote data server. * @param queueSize The queue size. * @param replicationServer The hosting RS. * @param rcvWindowSize The receiving window size. */ public DataServerHandler( Session session, int queueSize, ReplicationServer replicationServer, int rcvWindowSize) { super(session, queueSize, 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 = getStatusMachineEvent(newGenId); if (event == null) { return; } 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) logger.info(NOTE_BAD_GEN_ID_IN_FULL_UPDATE, replicationServer.getServerId(), getBaseDN(), serverId, generationId, newGenId); return; } changeStatus(event, "for reset gen id"); } private StatusMachineEvent getStatusMachineEvent(long newGenId) { if (newGenId == -1) { // The generation id is being made invalid, let's put the DS // into BAD_GEN_ID_STATUS return StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT; } if (newGenId != generationId) { // This server has a bad generation id compared to new reference one, // let's put it into BAD_GEN_ID_STATUS return StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT; } if (status != ServerStatus.BAD_GEN_ID_STATUS) { if (logger.isTraceEnabled()) { logger.trace("In RS " + replicationServer.getServerId() + ", DS " + getServerId() + " for baseDN=" + getBaseDN() + " has already generation id " + newGenId + " so no ChangeStatusMsg sent to him."); } return null; } // 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 (logger.isTraceEnabled()) { logger.trace("In RS " + replicationServer.getServerId() + ", closing connection to DS " + getServerId() + " for baseDN=" + getBaseDN() + " 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 releases it. if (session != null // V4 protocol introduced a StopMsg to properly close the // connection between servers && 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 null; } /** * Change the status according to the event. * * @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 changeStatus(StatusMachineEvent event) throws IOException { return changeStatus(event, "from status analyzer"); } private ServerStatus changeStatus(StatusMachineEvent event, String origin) throws IOException { // Check state machine allows this new status (Sanity check) ServerStatus newStatus = StatusMachine.computeNewStatus(status, event); if (newStatus == ServerStatus.INVALID_STATUS) { logger.error(ERR_RS_CANNOT_CHANGE_STATUS, getBaseDN(), serverId, status, event); // Only change allowed is from NORMAL_STATUS to DEGRADED_STATUS and vice // versa. We may be trying to change the status while 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, INVALID_STATUS); if (logger.isTraceEnabled()) { logger.trace("In RS " + replicationServer.getServerId() + " Sending change status " + origin + " to " + getServerId() + " for baseDN=" + getBaseDN() + ":\n" + csMsg); } session.publish(csMsg); status = newStatus; return newStatus; } /** {@inheritDoc} */ @Override public List<Attribute> getMonitorData() { // Get the generic ones List<Attribute> attributes = super.getMonitorData(); // Add the specific DS ones attributes.add(Attributes.create("replica", serverURL)); attributes.add(Attributes.create("connected-to", this.replicationServer.getMonitorInstanceName())); ReplicationDomainMonitorData md = replicationServerDomain.getDomainMonitorData(); // Oldest missing update long approxFirstMissingDate = md.getApproxFirstMissingDate(serverId); if (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 attributes.add(Attributes.create("missing-changes", String.valueOf(md.getMissingChanges(serverId)))); // Replication delay attributes.add(Attributes.create("approximate-delay", String.valueOf(md.getApproxDelay(serverId)))); /* get the Server State */ ServerState state = md.getLDAPServerState(serverId); if (state != null) { AttributeBuilder builder = new AttributeBuilder("server-state"); builder.addAllStrings(state.toStringSet()); 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. */ @Override 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) { logger.error(ERR_RS_INVALID_NEW_STATUS, reqStatus, getBaseDN(), serverId); return ServerStatus.INVALID_STATUS; } // Check state machine allows this new status ServerStatus newStatus = StatusMachine.computeNewStatus(status, event); if (newStatus == ServerStatus.INVALID_STATUS) { logger.error(ERR_RS_CANNOT_CHANGE_STATUS, getBaseDN(), serverId, status, event); 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 setBaseDNAndDomain(serverStartMsg.getBaseDN(), true); setInitialServerState(serverStartMsg.getServerState()); setSendWindowSize(serverStartMsg.getWindowSize()); if (heartbeatInterval < 0) { heartbeatInterval = 0; } return serverStartMsg.getSSLEncryption(); } /** 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; } } lockDomainNoTimeout(); localGenerationId = replicationServerDomain.getGenerationId(); oldGenerationId = localGenerationId; if (replicationServerDomain.isAlreadyConnectedToDS(this)) { abortStart(null); return; } try { StartMsg outStartMsg = sendStartToRemote(); 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) { LocalizableMessage errMessage = ERR_DS_DISCONNECTED_DURING_HANDSHAKE.get( inServerStartMsg.getServerId(), replicationServer.getServerId()); throw new DirectoryException(ResultCode.OTHER, errMessage); } 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); } replicationServerDomain.register(this); logger.debug(INFO_REPLICATION_SERVER_CONNECTION_FROM_DS, getReplicationServerId(), getServerId(), replicationServerDomain.getBaseDN(), session.getReadableRemoteAddress()); super.finalizeStart(); } catch(DirectoryException de) { abortStart(de.getMessageObject()); } catch(Exception e) { abortStart(null); } finally { releaseDomainLock(); } } /** * 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 = createReplServerStartMsg(); } else { // Peer DS uses protocol V4 : send it a ReplServerStartDSMsg startMsg = new ReplServerStartDSMsg(getReplicationServerId(), getReplicationServerURL(), getBaseDN(), maxRcvWindow, replicationServerDomain.getLatestServerState(), localGenerationId, sslEncryption, getLocalGroupId(), replicationServer.getDegradedStatusThreshold(), replicationServer.getWeight(), replicationServerDomain.getConnectedDSs().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, getReplicationServerId(), generationId, status, assuredFlag, assuredMode, safeDataLevel, groupId, refUrls, eclIncludes, eclIncludesForDeletes, getProtocolVersion()); } /** {@inheritDoc} */ @Override public String toString() { if (serverId != 0) { return "Replica DS(" + serverId + ") for domain \"" + replicationServerDomain.getBaseDN() + "\""; } 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 Exception */ private StartSessionMsg waitAndProcessStartSessionFromRemoteDS() throws Exception { 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)) { LocalizableMessage message = LocalizableMessage.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)) { throw new DirectoryException(ResultCode.OTHER, ERR_RS_INVALID_INIT_STATUS.get( this.status, getBaseDN(), serverId)); } 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) { logger.warn(WARN_BAD_GENERATION_ID_FROM_DS, serverId, session.getReadableRemoteAddress(), generationId, getBaseDN(), getReplicationServerId(), localGenerationId); } } 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 logger.warn(WARN_BAD_GENERATION_ID_FROM_DS, serverId, session.getReadableRemoteAddress(), generationId, getBaseDN(), getReplicationServerId(), localGenerationId); } 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); } } return startSessionMsg; } /** * Process message of a remote server changing his status. * @param csMsg The message containing the new status */ public void receiveNewStatus(ChangeStatusMsg csMsg) { replicationServerDomain.processNewStatus(this, csMsg); } }