001/*
002 * The contents of this file are subject to the terms of the Common Development and
003 * Distribution License (the License). You may not use this file except in compliance with the
004 * License.
005 *
006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
007 * specific language governing permission and limitations under the License.
008 *
009 * When distributing Covered Software, include this CDDL Header Notice in each file and include
010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
011 * Header, with the fields enclosed by brackets [] replaced by your own identifying
012 * information: "Portions Copyright [year] [name of copyright owner]".
013 *
014 * Copyright 2006-2010 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.replication.server;
018
019import static org.opends.messages.ReplicationMessages.*;
020import static org.opends.server.replication.common.ServerStatus.*;
021import static org.opends.server.replication.common.StatusMachine.*;
022import static org.opends.server.replication.protocol.ProtocolVersion.*;
023
024import java.io.IOException;
025import java.util.ArrayList;
026import java.util.Date;
027import java.util.HashSet;
028import java.util.List;
029import java.util.Set;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.slf4j.LocalizedLogger;
033import org.forgerock.opendj.ldap.ResultCode;
034import org.opends.server.api.MonitorData;
035import org.opends.server.replication.common.AssuredMode;
036import org.opends.server.replication.common.DSInfo;
037import org.opends.server.replication.common.ServerState;
038import org.opends.server.replication.common.ServerStatus;
039import org.opends.server.replication.common.StatusMachine;
040import org.opends.server.replication.common.StatusMachineEvent;
041import org.opends.server.replication.protocol.ChangeStatusMsg;
042import org.opends.server.replication.protocol.ProtocolVersion;
043import org.opends.server.replication.protocol.ReplServerStartDSMsg;
044import org.opends.server.replication.protocol.ReplicationMsg;
045import org.opends.server.replication.protocol.ServerStartMsg;
046import org.opends.server.replication.protocol.Session;
047import org.opends.server.replication.protocol.StartMsg;
048import org.opends.server.replication.protocol.StartSessionMsg;
049import org.opends.server.replication.protocol.StopMsg;
050import org.opends.server.replication.protocol.TopologyMsg;
051import org.opends.server.types.DirectoryException;
052
053/**
054 * This class defines a server handler, which handles all interaction with a
055 * peer server (RS or DS).
056 */
057public class DataServerHandler extends ServerHandler
058{
059
060  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
061
062  /**
063   * Temporary generationId received in handshake/phase1, and used after
064   * handshake/phase2.
065   */
066  private long tmpGenerationId;
067
068  /** Status of this DS (only used if this server handler represents a DS). */
069  private ServerStatus status = ServerStatus.INVALID_STATUS;
070
071  /** Referrals URLs this DS is exporting. */
072  private List<String> refUrls = new ArrayList<>();
073  /** Assured replication enabled on DS or not. */
074  private boolean assuredFlag;
075  /** DS assured mode (relevant if assured replication enabled). */
076  private AssuredMode assuredMode = AssuredMode.SAFE_DATA_MODE;
077  /** DS safe data level (relevant if assured mode is safe data). */
078  private byte safeDataLevel = -1;
079  private Set<String> eclIncludes = new HashSet<>();
080  private Set<String> eclIncludesForDeletes = new HashSet<>();
081
082  /**
083   * Creates a new data server handler.
084   * @param session The session opened with the remote data server.
085   * @param queueSize The queue size.
086   * @param replicationServer The hosting RS.
087   * @param rcvWindowSize The receiving window size.
088   */
089  public DataServerHandler(
090      Session session,
091      int queueSize,
092      ReplicationServer replicationServer,
093      int rcvWindowSize)
094  {
095    super(session, queueSize, replicationServer, rcvWindowSize);
096  }
097
098  /**
099   * Order the peer DS server to change his status or close the connection
100   * according to the requested new generation id.
101   * @param newGenId The new generation id to take into account
102   * @throws IOException If IO error occurred.
103   */
104  public void changeStatusForResetGenId(long newGenId) throws IOException
105  {
106    StatusMachineEvent event = getStatusMachineEvent(newGenId);
107    if (event == null)
108    {
109      return;
110    }
111
112    if (event == StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT
113        && status == ServerStatus.FULL_UPDATE_STATUS)
114    {
115      // Prevent useless error message (full update status cannot lead to bad gen status)
116      logger.info(NOTE_BAD_GEN_ID_IN_FULL_UPDATE, replicationServer.getServerId(),
117          getBaseDN(), serverId, generationId, newGenId);
118      return;
119    }
120
121    changeStatus(event, "for reset gen id");
122  }
123
124  private StatusMachineEvent getStatusMachineEvent(long newGenId)
125  {
126    if (newGenId == -1)
127    {
128      // The generation id is being made invalid, let's put the DS
129      // into BAD_GEN_ID_STATUS
130      return StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT;
131    }
132    if (newGenId != generationId)
133    {
134      // This server has a bad generation id compared to new reference one,
135      // let's put it into BAD_GEN_ID_STATUS
136      return StatusMachineEvent.TO_BAD_GEN_ID_STATUS_EVENT;
137    }
138
139    if (status != ServerStatus.BAD_GEN_ID_STATUS)
140    {
141      if (logger.isTraceEnabled())
142      {
143        logger.trace("In RS " + replicationServer.getServerId()
144            + ", DS " + getServerId() + " for baseDN=" + getBaseDN()
145            + " has already generation id " + newGenId
146            + " so no ChangeStatusMsg sent to him.");
147      }
148      return null;
149    }
150
151    // This server has the good new reference generation id.
152    // Close connection with him to force his reconnection: DS will
153    // reconnect in NORMAL_STATUS or DEGRADED_STATUS.
154
155    if (logger.isTraceEnabled())
156    {
157      logger.trace("In RS " + replicationServer.getServerId()
158          + ", closing connection to DS " + getServerId() + " for baseDN=" + getBaseDN()
159          + " to force reconnection as new local generationId"
160          + " and remote one match and DS is in bad gen id: " + newGenId);
161    }
162
163    // Connection closure must not be done calling RSD.stopHandler() as it
164    // would rewait the RSD lock that we already must have entering this
165    // method. This would lead to a reentrant lock which we do not want.
166    // So simply close the session, this will make the hang up appear
167    // after the reader thread that took the RSD lock releases it.
168    if (session != null
169        // V4 protocol introduced a StopMsg to properly close the
170        // connection between servers
171        && getProtocolVersion() >= ProtocolVersion.REPLICATION_PROTOCOL_V4)
172    {
173      try
174      {
175        session.publish(new StopMsg());
176      }
177      catch (IOException ioe)
178      {
179        // Anyway, going to close session, so nothing to do
180      }
181    }
182
183    // NOT_CONNECTED_STATUS is the last one in RS session life: handler
184    // will soon disappear after this method call...
185    status = ServerStatus.NOT_CONNECTED_STATUS;
186    return null;
187  }
188
189  /**
190   * Change the status according to the event.
191   *
192   * @param event
193   *          The event to be used for new status computation
194   * @return The new status of the DS
195   * @throws IOException
196   *           When raised by the underlying session
197   */
198  public ServerStatus changeStatus(StatusMachineEvent event) throws IOException
199  {
200    return changeStatus(event, "from status analyzer");
201  }
202
203  private ServerStatus changeStatus(StatusMachineEvent event, String origin)
204      throws IOException
205  {
206    // Check state machine allows this new status (Sanity check)
207    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
208    if (newStatus == ServerStatus.INVALID_STATUS)
209    {
210      logger.error(ERR_RS_CANNOT_CHANGE_STATUS, getBaseDN(), serverId, status, event);
211      // Only change allowed is from NORMAL_STATUS to DEGRADED_STATUS and vice
212      // versa. We may be trying to change the status while another status has
213      // just been entered: e.g a full update has just been engaged.
214      // In that case, just ignore attempt to change the status
215      return newStatus;
216    }
217
218    // Send message requesting to change the DS status
219    ChangeStatusMsg csMsg = new ChangeStatusMsg(newStatus, INVALID_STATUS);
220
221    if (logger.isTraceEnabled())
222    {
223      logger.trace("In RS " + replicationServer.getServerId()
224          + " Sending change status " + origin + " to " + getServerId()
225          + " for baseDN=" + getBaseDN() + ":\n" + csMsg);
226    }
227
228    session.publish(csMsg);
229
230    status = newStatus;
231
232    return newStatus;
233  }
234
235  @Override
236  public MonitorData getMonitorData()
237  {
238    MonitorData attributes = super.getMonitorData();
239
240    // Add the specific DS ones
241    attributes.add("replica", serverURL);
242    attributes.add("connected-to", replicationServer.getMonitorInstanceName());
243
244    ReplicationDomainMonitorData md = replicationServerDomain.getDomainMonitorData();
245
246    // Oldest missing update
247    long approxFirstMissingDate = md.getApproxFirstMissingDate(serverId);
248    if (approxFirstMissingDate > 0)
249    {
250      attributes.add("approx-older-change-not-synchronized", new Date(approxFirstMissingDate));
251      attributes.add("approx-older-change-not-synchronized-millis", approxFirstMissingDate);
252    }
253
254    attributes.add("missing-changes", md.getMissingChanges(serverId));
255    // Replication delay
256    attributes.add("approximate-delay", md.getApproxDelay(serverId));
257
258    ServerState state = md.getLDAPServerState(serverId);
259    if (state != null)
260    {
261      attributes.add("server-state", state.toStringSet());
262    }
263
264    return attributes;
265  }
266
267  @Override
268  public String getMonitorInstanceName()
269  {
270    return "Connected directory server DS(" + serverId + ") " + serverURL
271        + ",cn=" + replicationServerDomain.getMonitorInstanceName();
272  }
273
274  /**
275   * Gets the status of the connected DS.
276   * @return The status of the connected DS.
277   */
278  @Override
279  public ServerStatus getStatus()
280  {
281    return status;
282  }
283
284  @Override
285  public boolean isDataServer()
286  {
287    return true;
288  }
289
290  /**
291   * Process message of a remote server changing his status.
292   * @param csMsg The message containing the new status
293   * @return The new server status of the DS
294   */
295  public ServerStatus processNewStatus(ChangeStatusMsg csMsg)
296  {
297    // Get the status the DS just entered
298    ServerStatus reqStatus = csMsg.getNewStatus();
299    // Translate new status to a state machine event
300    StatusMachineEvent event = StatusMachineEvent.statusToEvent(reqStatus);
301    if (event == StatusMachineEvent.INVALID_EVENT)
302    {
303      logger.error(ERR_RS_INVALID_NEW_STATUS, reqStatus, getBaseDN(), serverId);
304      return ServerStatus.INVALID_STATUS;
305    }
306
307    // Check state machine allows this new status
308    ServerStatus newStatus = StatusMachine.computeNewStatus(status, event);
309    if (newStatus == ServerStatus.INVALID_STATUS)
310    {
311      logger.error(ERR_RS_CANNOT_CHANGE_STATUS, getBaseDN(), serverId, status, event);
312      return ServerStatus.INVALID_STATUS;
313    }
314
315    status = newStatus;
316    return status;
317  }
318
319  /**
320   * Processes a start message received from a remote data server.
321   * @param serverStartMsg The provided start message received.
322   * @return flag specifying whether the remote server requests encryption.
323   * @throws DirectoryException raised when an error occurs.
324   */
325  public boolean processStartFromRemote(ServerStartMsg serverStartMsg)
326  throws DirectoryException
327  {
328    session
329        .setProtocolVersion(getCompatibleVersion(serverStartMsg.getVersion()));
330    tmpGenerationId = serverStartMsg.getGenerationId();
331    serverId = serverStartMsg.getServerId();
332    serverURL = serverStartMsg.getServerURL();
333    groupId = serverStartMsg.getGroupId();
334    heartbeatInterval = serverStartMsg.getHeartbeatInterval();
335
336    // generic stuff
337    setBaseDNAndDomain(serverStartMsg.getBaseDN(), true);
338    setInitialServerState(serverStartMsg.getServerState());
339    setSendWindowSize(serverStartMsg.getWindowSize());
340
341    if (heartbeatInterval < 0)
342    {
343      heartbeatInterval = 0;
344    }
345    return serverStartMsg.getSSLEncryption();
346  }
347
348  /** Send our own TopologyMsg to DS. */
349  private TopologyMsg sendTopoToRemoteDS() throws IOException
350  {
351    TopologyMsg outTopoMsg = replicationServerDomain
352        .createTopologyMsgForDS(this.serverId);
353    sendTopoInfo(outTopoMsg);
354    return outTopoMsg;
355  }
356
357  /**
358   * Starts the handler from a remote ServerStart message received from
359   * the remote data server.
360   * @param inServerStartMsg The provided ServerStart message received.
361   */
362  public void startFromRemoteDS(ServerStartMsg inServerStartMsg)
363  {
364    try
365    {
366      // initializations
367      localGenerationId = -1;
368      oldGenerationId = -100;
369
370      // processes the ServerStart message received
371      boolean sessionInitiatorSSLEncryption =
372        processStartFromRemote(inServerStartMsg);
373
374      /**
375       * Hack to be sure that if a server disconnects and reconnect, we
376       * let the reader thread see the closure and cleanup any reference
377       * to old connection. This must be done before taking the domain lock so
378       * that the reader thread has a chance to stop the handler.
379       *
380       * TODO: This hack should be removed and disconnection/reconnection
381       * properly dealt with.
382       */
383      if (replicationServerDomain.getConnectedDSs()
384          .containsKey(inServerStartMsg.getServerId()))
385      {
386        try {
387          Thread.sleep(100);
388        }
389        catch(Exception e){
390          abortStart(null);
391          return;
392        }
393      }
394
395      lockDomainNoTimeout();
396
397      localGenerationId = replicationServerDomain.getGenerationId();
398      oldGenerationId = localGenerationId;
399
400      if (replicationServerDomain.isAlreadyConnectedToDS(this))
401      {
402        abortStart(null);
403        return;
404      }
405
406      try
407      {
408        StartMsg outStartMsg = sendStartToRemote();
409
410        logStartHandshakeRCVandSND(inServerStartMsg, outStartMsg);
411
412        // The session initiator decides whether to use SSL.
413        // Until here session is encrypted then it depends on the negotiation
414        if (!sessionInitiatorSSLEncryption)
415        {
416          session.stopEncryption();
417        }
418
419        // wait and process StartSessionMsg from remote RS
420        StartSessionMsg inStartSessionMsg =
421          waitAndProcessStartSessionFromRemoteDS();
422        if (inStartSessionMsg == null)
423        {
424          // DS wants to properly close the connection (DS sent a StopMsg)
425          logStopReceived();
426          abortStart(null);
427          return;
428        }
429
430        // Send our own TopologyMsg to remote DS
431        TopologyMsg outTopoMsg = sendTopoToRemoteDS();
432
433        logStartSessionHandshake(inStartSessionMsg, outTopoMsg);
434      }
435      catch(IOException e)
436      {
437        LocalizableMessage errMessage = ERR_DS_DISCONNECTED_DURING_HANDSHAKE.get(
438            inServerStartMsg.getServerId(), replicationServer.getServerId());
439        throw new DirectoryException(ResultCode.OTHER, errMessage);
440      }
441      catch (Exception e)
442      {
443        // We do not need to support DS V1 connection, we just accept RS V1
444        // connection:
445        // We just trash the message, log the event for debug purpose and close
446        // the connection
447        throw new DirectoryException(ResultCode.OTHER, null, null);
448      }
449
450      replicationServerDomain.register(this);
451
452      logger.debug(INFO_REPLICATION_SERVER_CONNECTION_FROM_DS, getReplicationServerId(), getServerId(),
453              replicationServerDomain.getBaseDN(), session.getReadableRemoteAddress());
454
455      super.finalizeStart();
456    }
457    catch(DirectoryException de)
458    {
459      abortStart(de.getMessageObject());
460    }
461    catch(Exception e)
462    {
463      abortStart(null);
464    }
465    finally
466    {
467      releaseDomainLock();
468    }
469  }
470
471  /**
472   * Sends a start message to the remote DS.
473   *
474   * @return The StartMsg sent.
475   * @throws IOException
476   *           When an exception occurs.
477   */
478  private StartMsg sendStartToRemote() throws IOException
479  {
480    final StartMsg startMsg;
481
482    // Before V4 protocol, we sent a ReplServerStartMsg
483    if (getProtocolVersion() < ProtocolVersion.REPLICATION_PROTOCOL_V4)
484    {
485      // Peer DS uses protocol < V4 : send it a ReplServerStartMsg
486      startMsg = createReplServerStartMsg();
487    }
488    else
489    {
490      // Peer DS uses protocol V4 : send it a ReplServerStartDSMsg
491      startMsg = new ReplServerStartDSMsg(getReplicationServerId(),
492          getReplicationServerURL(), getBaseDN(), maxRcvWindow,
493          replicationServerDomain.getLatestServerState(),
494          localGenerationId, sslEncryption, getLocalGroupId(),
495          replicationServer.getDegradedStatusThreshold(),
496          replicationServer.getWeight(),
497          replicationServerDomain.getConnectedDSs().size());
498    }
499
500    send(startMsg);
501    return startMsg;
502  }
503
504  /**
505   * Creates a DSInfo structure representing this remote DS.
506   * @return The DSInfo structure representing this remote DS
507   */
508  public DSInfo toDSInfo()
509  {
510    return new DSInfo(serverId, serverURL, getReplicationServerId(),
511        generationId, status, assuredFlag, assuredMode, safeDataLevel, groupId,
512        refUrls, eclIncludes, eclIncludesForDeletes, getProtocolVersion());
513  }
514
515  @Override
516  public String toString()
517  {
518    if (serverId != 0)
519    {
520      return "Replica DS(" + serverId + ") for domain \""
521          + replicationServerDomain.getBaseDN() + "\"";
522    }
523    return "Unknown server";
524  }
525
526  /**
527   * Wait receiving the StartSessionMsg from the remote DS and process it, or
528   * receiving a StopMsg to properly stop the handshake procedure.
529   * @return the startSessionMsg received or null DS sent a stop message to
530   *         not finish the handshake.
531   * @throws Exception
532   */
533  private StartSessionMsg waitAndProcessStartSessionFromRemoteDS()
534      throws Exception
535  {
536    ReplicationMsg msg = session.receive();
537
538    if (msg instanceof StopMsg)
539    {
540      // DS wants to stop handshake (was just for handshake phase one for RS
541      // choice). Return null to make the session be terminated.
542      return null;
543    } else if (!(msg instanceof StartSessionMsg))
544    {
545      LocalizableMessage message = LocalizableMessage.raw(
546          "Protocol error: StartSessionMsg required." + msg + " received.");
547      abortStart(message);
548      return null;
549    }
550
551    // Process StartSessionMsg sent by remote DS
552    StartSessionMsg startSessionMsg = (StartSessionMsg) msg;
553
554    this.status = startSessionMsg.getStatus();
555    // Sanity check: is it a valid initial status?
556    if (!isValidInitialStatus(this.status))
557    {
558      throw new DirectoryException(ResultCode.OTHER,
559          ERR_RS_INVALID_INIT_STATUS.get( this.status, getBaseDN(), serverId));
560    }
561
562    this.refUrls = startSessionMsg.getReferralsURLs();
563    this.assuredFlag = startSessionMsg.isAssured();
564    this.assuredMode = startSessionMsg.getAssuredMode();
565    this.safeDataLevel = startSessionMsg.getSafeDataLevel();
566    this.eclIncludes = startSessionMsg.getEclIncludes();
567    this.eclIncludesForDeletes = startSessionMsg.getEclIncludesForDeletes();
568
569    /*
570     * If we have already a generationID set for the domain
571     * then
572     *   if the connecting replica has not the same
573     *   then it is degraded locally and notified by an error message
574     * else
575     *   we set the generationID from the one received
576     *   (unsaved yet on disk . will be set with the 1rst change
577     * received)
578     */
579    generationId = tmpGenerationId;
580    if (localGenerationId > 0)
581    {
582      if (generationId != localGenerationId)
583      {
584        logger.warn(WARN_BAD_GENERATION_ID_FROM_DS, serverId, session.getReadableRemoteAddress(),
585            generationId, getBaseDN(), getReplicationServerId(), localGenerationId);
586      }
587    }
588    else
589    {
590      // We are an empty ReplicationServer
591      if (generationId > 0 && !getServerState().isEmpty())
592      {
593        // If the LDAP server has already sent changes
594        // it is not expected to connect to an empty RS
595        logger.warn(WARN_BAD_GENERATION_ID_FROM_DS, serverId, session.getReadableRemoteAddress(),
596            generationId, getBaseDN(), getReplicationServerId(), localGenerationId);
597      }
598      else
599      {
600        // The local RS is not initialized - take the one received
601        // WARNING: Must be done before computing topo message to send
602        // to peer server as topo message must embed valid generation id
603        // for our server
604        oldGenerationId = replicationServerDomain.changeGenerationId(generationId);
605      }
606    }
607    return startSessionMsg;
608  }
609
610  /**
611   * Process message of a remote server changing his status.
612   * @param csMsg The message containing the new status
613   */
614  public void receiveNewStatus(ChangeStatusMsg csMsg)
615  {
616    replicationServerDomain.processNewStatus(this, csMsg);
617  }
618}