opends/resource/config/config.ldif
@@ -78,6 +78,7 @@ ds-cfg-global-aci: (target="ldap:///")(targetscope="base")(targetattr="objectClass||namingContexts||supportedAuthPasswordSchemes||supportedControl||supportedExtension||supportedFeatures||supportedLDAPVersion||supportedSASLMechanisms||vendorName||vendorVersion")(version 3.0; acl "User-Visible Root DSE Operational Attributes"; allow (read,search,compare) userdn="ldap:///anyone";) ds-cfg-global-aci: (targetattr="createTimestamp||creatorsName||modifiersName||modifyTimestamp||entryDN||entryUUID||subschemaSubentry")(version 3.0; acl "User-Visible Operational Attributes"; allow (read,search,compare) userdn="ldap:///anyone";) ds-cfg-global-aci: (target="ldap:///dc=replicationchanges")(targetattr="*")(version 3.0; acl "Replication backend access"; deny (all) userdn="ldap:///anyone";) ds-cfg-global-aci: (target="ldap:///cn=changelog")(targetattr="*")(version 3.0; acl "External changelog access"; deny (all) userdn="ldap:///anyone";) cn: Access Control Handler ds-cfg-java-class: org.opends.server.authorization.dseecompat.AciHandler ds-cfg-enabled: true opends/src/messages/messages/protocol.properties
@@ -1411,4 +1411,7 @@ SEVERE_ERR_SNMP_CONNHANDLER_OPENDMK_JARFILES_NOT_OPERATIONAL_1507=The required \ classes could not be loaded using jar file '%s'. Verify that the jar file \ is not corrupted MILD_ERR_CANNOT_DECODE_CONTROL_VALUE_1508=Cannot decode the provided \ control %s because an error occurred while attempting to \ decode the control value: %s opends/src/messages/messages/replication.properties
@@ -365,4 +365,11 @@ SEVERE_ERR_COULD_NOT_START_REPLICATION_154=The Replication was not started \ on base-dn %s : %s MILD_ERR_ERROR_RETRIEVING_MONITOR_DATA_155=Error retrieving monitoring data: %s SEVERE_ERR_BYTE_COUNT_156=The Server Handler byte count is not correct (Fixed) SEVERE_ERR_EXCEPTION_LOCKING_RS_DOMAIN_156=Caught exception when locking \ the replication server domain: %s SEVERE_ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE_157=Replication \ protocol error. Bad message type. %s received, %s required SEVERE_ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED_158=The provided cookie is \ valid in the current context due to %s. Full resync is required SEVERE_ERR_BYTE_COUNT_159=The Server Handler byte count is not correct (Fixed) opends/src/server/org/opends/server/controls/EntryChangelogNotificationControl.java
New file @@ -0,0 +1,173 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.controls; import static org.opends.messages.ProtocolMessages.ERR_ECN_CANNOT_DECODE_VALUE; import static org.opends.messages.ProtocolMessages.ERR_ECN_NO_CONTROL_VALUE; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import static org.opends.server.protocols.asn1.ASN1Constants.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.getExceptionMessage; import java.io.IOException; import org.opends.messages.Message; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.asn1.ASN1; import org.opends.server.protocols.asn1.ASN1Reader; import org.opends.server.protocols.asn1.ASN1Writer; import org.opends.server.types.ByteString; import org.opends.server.types.Control; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.ResultCode; /** * This class implements the ECL cookie control. * It may be included in entries returned in response to a search or * persistent search operation. */ public class EntryChangelogNotificationControl extends Control { // The tracer object for the debug logger. private static final DebugTracer TRACER = getTracer(); // The cookie value - payload of this control. private String cookie; /** * ControlDecoder implentation to decode this control from a ByteString. */ private final static class Decoder implements ControlDecoder<EntryChangelogNotificationControl> { /** * {@inheritDoc} */ public EntryChangelogNotificationControl decode( boolean isCritical, ByteString value) throws DirectoryException { if (value == null) { // FIXME:ECL Create error messages dedicated to this control Message message = ERR_ECN_NO_CONTROL_VALUE.get(); throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message); } String cookie = null; ASN1Reader reader = ASN1.getReader(value); try { reader.readStartSequence(); cookie = reader.readOctetStringAsString(); } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } Message message = ERR_ECN_CANNOT_DECODE_VALUE.get(getExceptionMessage(e)); throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message, e); } return new EntryChangelogNotificationControl(isCritical, cookie); } public String getOID() { return OID_ECL_COOKIE_EXCHANGE_CONTROL; } } /** * The Control Decoder that can be used to decode this control. */ public static final ControlDecoder<EntryChangelogNotificationControl> DECODER = new Decoder(); /** * Creates a new entry change notification control with the provided * information. * * @param isCritical Indicates whether this control should be * considered critical in processing the * request. * @param cookie The provided cookie value. */ public EntryChangelogNotificationControl(boolean isCritical, String cookie) { super(OID_ECL_COOKIE_EXCHANGE_CONTROL, isCritical); this.cookie = cookie; } /** * Writes this control's value to an ASN.1 writer. The value (if any) * must be written as an ASN1OctetString. * * @param writer The ASN.1 output stream to write to. * @throws IOException If a problem occurs while writing to the stream. */ public void writeValue(ASN1Writer writer) throws IOException { writer.writeStartSequence(UNIVERSAL_OCTET_STRING_TYPE); writer.writeOctetString(cookie.toString()); writer.writeEndSequence(); } /** * Retrieves the change type for this entry change notification control. * * @return The change type for this entry change notification control. */ public String getCookie() { return cookie; } /** * Appends a string representation of this entry change notification control * to the provided buffer. * * @param buffer The buffer to which the information should be appended. */ public void toString(StringBuilder buffer) { buffer.append("EntryChangelogNotificationControl(cookie="); buffer.append(cookie.toString()); buffer.append(")"); } } opends/src/server/org/opends/server/controls/ExternalChangelogRequestControl.java
New file @@ -0,0 +1,170 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.controls; import static org.opends.messages.ProtocolMessages.*; import static org.opends.server.protocols.asn1.ASN1Constants.*; import static org.opends.server.util.ServerConstants.*; import static org.opends.server.util.StaticUtils.getExceptionMessage; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import java.io.IOException; import org.opends.messages.Message; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.asn1.ASN1; import org.opends.server.protocols.asn1.ASN1Reader; import org.opends.server.protocols.asn1.ASN1Writer; import org.opends.server.replication.common.MultiDomainServerState; import org.opends.server.types.ByteString; import org.opends.server.types.Control; import org.opends.server.types.DirectoryException; import org.opends.server.types.ResultCode; /** * This class implements the persistent search control defined in * draft-ietf-ldapext-psearch. It makes it possible for clients to be notified * of changes to information in the Directory Server as they occur. */ public class ExternalChangelogRequestControl extends Control { private static final DebugTracer TRACER = getTracer(); private MultiDomainServerState cookie; /** * ControlDecoder implentation to decode this control from a ByteString. */ private final static class Decoder implements ControlDecoder<ExternalChangelogRequestControl> { /** * {@inheritDoc} */ public ExternalChangelogRequestControl decode(boolean isCritical, ByteString value) throws DirectoryException { if (value == null) { // FIXME:ECL In the request cookie, empty value is currently rejected. Message message = ERR_PSEARCH_NO_CONTROL_VALUE.get(); throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message); } ASN1Reader reader = ASN1.getReader(value); MultiDomainServerState mdss; String mdssValue = null; try { mdssValue = reader.readOctetStringAsString(); mdss = new MultiDomainServerState(mdssValue); } catch (Exception e) { try { mdssValue = value.toString(); mdss = new MultiDomainServerState(mdssValue); } catch (Exception e2) { Message message = ERR_CANNOT_DECODE_CONTROL_VALUE.get( getOID() + " x=" + value.toHex() + " v=" + mdssValue , getExceptionMessage(e).toString()); throw new DirectoryException(ResultCode.PROTOCOL_ERROR, message, e); } } return new ExternalChangelogRequestControl(isCritical, mdss); } public String getOID() { return OID_ECL_COOKIE_EXCHANGE_CONTROL; } } /** * The Control Decoder that can be used to decode this control. */ public static final ControlDecoder<ExternalChangelogRequestControl> DECODER = new Decoder(); /** * Create a new external change log request control to contain the cookie. * @param isCritical Specifies whether the control is critical. * @param cookie Specifies the cookie value. */ public ExternalChangelogRequestControl(boolean isCritical, MultiDomainServerState cookie) { super(OID_ECL_COOKIE_EXCHANGE_CONTROL, isCritical); this.cookie = cookie; } /** * Returns the cookie value. * @return The cookie value. */ public MultiDomainServerState getCookie() { return this.cookie; } /** * Dump a string representation of this object to the provided bufer. * @param buffer The provided buffer. */ public void toString(StringBuilder buffer) { buffer.append("ExternalChangelogRequestControl(cookie="); this.cookie.toString(buffer); buffer.append(")"); } /** * Writes this control's value to an ASN.1 writer. The value * (if any) must be written as an ASN1OctetString. * * @param writer The ASN.1 writer to use. * @throws IOException If a problem occurs while writing to the * stream. */ protected void writeValue(ASN1Writer writer) throws IOException { writer.writeStartSequence(UNIVERSAL_OCTET_STRING_TYPE); writer.writeOctetString(this.cookie.toString()); writer.writeEndSequence(); } } opends/src/server/org/opends/server/core/PersistentSearch.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Copyright 2006-2009 Sun Microsystems, Inc. */ package org.opends.server.core; @@ -240,6 +240,15 @@ } /** * Get the search operation associated with this persistent search. * * @return The search operation associated with this persistent search. */ public SearchOperation getSearchOperation() { return searchOperation; } /** * Notifies the persistent searches that an entry has been added. opends/src/server/org/opends/server/loggers/debug/DebugLogger.java
@@ -453,7 +453,7 @@ */ public static boolean debugEnabled() { return enabled; return true; } /** opends/src/server/org/opends/server/replication/common/ExternalChangeLogSession.java
New file @@ -0,0 +1,52 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.replication.common; import org.opends.server.replication.protocol.ECLUpdateMsg; import org.opends.server.types.DirectoryException; /** * This interface defines a session used to search the external changelog * in the Directory Server. */ public interface ExternalChangeLogSession { /** * Returns the next message available for the ECL (blocking). * @return the next available message from the ECL. * @throws DirectoryException When an error occurs. */ public abstract ECLUpdateMsg getNextUpdate() throws DirectoryException; /** * Closes the session. * @throws DirectoryException when needed. */ public abstract void close() throws DirectoryException; } opends/src/server/org/opends/server/replication/common/MultiDomainServerState.java
New file @@ -0,0 +1,215 @@ /* * 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-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.common; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import java.util.Iterator; import java.util.TreeMap; import org.opends.server.loggers.debug.DebugTracer; /** * This object is used to store a list of ServerState object, one by * replication domain. Globally, it is the generalization of ServerState * (that applies to one domain) to a list of domains. */ public class MultiDomainServerState implements Iterable<String> { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * The list of (domain service id, ServerState). */ private TreeMap<String, ServerState> list; /** * Creates a new empty object. */ public MultiDomainServerState() { list = new TreeMap<String, ServerState>(); } /** * Empty the object.. * After this call the object will be in the same state as if it * was just created. */ public void clear() { synchronized (this) { list.clear(); } } /** * Update the ServerState of the provided serviceId with the * replication change number provided. * * @param serviceId The provided serviceId. * @param changeNumber The provided ChangeNumber. * * @return a boolean indicating if the update was meaningful. */ public boolean update(String serviceId, ChangeNumber changeNumber) { if (changeNumber == null) return false; synchronized(this) { Short serverId = changeNumber.getServerId(); ServerState oldServerState = list.get(serviceId); if (oldServerState == null) oldServerState = new ServerState(); if (changeNumber.newer(oldServerState.getMaxChangeNumber(serverId))) { oldServerState.update(changeNumber); list.put(serviceId,oldServerState); return true; } else { return false; } } } /** * Create an object from a string representation. * @param mdss The provided string representation of the state. */ public MultiDomainServerState(String mdss) { list = new TreeMap<String, ServerState>(); if ((mdss != null) && (mdss.length()>0)) { String[] domains = mdss.split(";"); for (String domain : domains) { String[] fields = domain.split(":"); String name = fields[0]; ServerState ss = new ServerState(); if (fields.length>1) { String strState = fields[1].trim(); String[] strCN = strState.split(" "); for (String sr : strCN) { ChangeNumber fromChangeNumber = new ChangeNumber(sr); ss.update(fromChangeNumber); } } this.list.put(name, ss); } } } /** * Returns a string representation of this object. * @return The string representation. */ public String toString() { String res = ""; if ((list != null) && (!list.isEmpty())) { for (String serviceId : list.keySet()) { ServerState ss = list.get(serviceId); res += serviceId + ":" + ss.toString(); res += ";"; } } return res; } /** * Dump a string representation in the provided buffer. * @param buffer The provided buffer. */ public void toString(StringBuilder buffer) { buffer.append(this.toString()); } /** * Tests if the state is empty. * * @return True if the state is empty. */ public boolean isEmpty() { return list.isEmpty(); } /** * {@inheritDoc} */ public Iterator<String> iterator() { return list.keySet().iterator(); } /** * Test if this object equals the provided other object. * @param other The other object with which we want to test equality. * @return Returns True if this equals other, else return false. */ public boolean equalsTo(MultiDomainServerState other) { return ((this.cover(other)) && (other.cover(this))); } /** * Test if this object covers the provided covered object. * @param covered The provided object. * @return true when this covers the provided object. */ public boolean cover(MultiDomainServerState covered) { for (String serviceId : covered.list.keySet()) { ServerState state = list.get(serviceId); ServerState coveredState = covered.list.get(serviceId); if ((coveredState == null) || (!state.cover(coveredState))) { return false; } } return true; } } opends/src/server/org/opends/server/replication/common/ServerState.java
@@ -122,6 +122,8 @@ /** * Get the length of the next String encoded in the in byte array. * This method is used to cut the different parts (server ids, change number) * of a server state. * * @param in the byte array where to calculate the string. * @param pos the position whre to start from in the byte array. @@ -135,7 +137,7 @@ while (in[offset++] != 0) { if (offset >= in.length) throw new DataFormatException("byte[] is not a valid modify msg"); throw new DataFormatException("byte[] is not a valid server state"); length++; } return length; @@ -251,7 +253,6 @@ * Return the text representation of ServerState. * @return the text representation of ServerState */ @Override public String toString() { StringBuilder buffer = new StringBuilder(); @@ -261,9 +262,11 @@ for (Short key : list.keySet()) { ChangeNumber change = list.get(key); buffer.append(change.toString()); buffer.append(" "); buffer.append(change.toStringUI()); } if (!list.isEmpty()) buffer.deleteCharAt(buffer.length()-1); } return buffer.toString(); opends/src/server/org/opends/server/replication/plugin/MultimasterReplication.java
@@ -817,4 +817,23 @@ { return domains.size(); } /** * Gets the baseDn of the domains that have a private backend. * @return The private baseDN. */ public static ArrayList<String> getPrivateDomains() { ArrayList<String> privateDNs = new ArrayList<String>(); for (LDAPReplicationDomain domain : domains.values()) { Backend b = domain.getBackend(); if (b != null) if (b.isPrivateBackend()) privateDNs.add(domain.getBaseDN().toNormalizedString()); } return privateDNs; } } opends/src/server/org/opends/server/replication/protocol/ECLUpdateMsg.java
New file @@ -0,0 +1,189 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.replication.protocol; import java.io.UnsupportedEncodingException; import java.util.zip.DataFormatException; import org.opends.server.replication.common.MultiDomainServerState; /** * Container for the ECL information sent from the ReplicationServer * to the client part (either broker over the protocol, or ECLSession). */ public class ECLUpdateMsg extends ReplicationMsg { // The replication change returned. private final LDAPUpdateMsg updateMsg; // The serviceId (baseDN) of the domain to which applies the change. private final String serviceId; // The value of the cookie updated with the current change private MultiDomainServerState cookie; /** * Creates a new message. * @param update The provided update. * @param cookie The provided cookie value * @param serviceId The provided serviceId. */ public ECLUpdateMsg(LDAPUpdateMsg update, MultiDomainServerState cookie, String serviceId) { this.cookie = cookie; this.serviceId = serviceId; this.updateMsg = update; } /** * Creates a new message from its encoded form. * * @param in The byte array containing the encoded form of the message. * @throws DataFormatException If the byte array does not contain * a valid encoded form of the message. * @throws UnsupportedEncodingException when it occurs. * @throws NotSupportedOldVersionPDUException when it occurs. */ public ECLUpdateMsg(byte[] in) throws DataFormatException, UnsupportedEncodingException, NotSupportedOldVersionPDUException { try { if (in[0] != MSG_TYPE_ECL_UPDATE) { throw new DataFormatException("byte[] is not a valid " + this.getClass().getCanonicalName()); } int pos = 1; // Decode the cookie int length = getNextLength(in, pos); String cookieStr = new String(in, pos, length, "UTF-8"); this.cookie = new MultiDomainServerState(cookieStr); pos += length + 1; // Decode the serviceId length = getNextLength(in, pos); this.serviceId = new String(in, pos, length, "UTF-8"); pos += length + 1; // Decode the msg /* Read the mods : all the remaining bytes but the terminating 0 */ length = in.length - pos - 1; byte[] encodedMsg = new byte[length]; System.arraycopy(in, pos, encodedMsg, 0, length); ReplicationMsg rmsg = ReplicationMsg.generateMsg(encodedMsg); this.updateMsg = (LDAPUpdateMsg)rmsg; } catch (UnsupportedEncodingException e) { throw new DataFormatException("UTF-8 is not supported by this jvm."); } } /** * Getter for the cookie value. * @return The cookie value. */ public MultiDomainServerState getCookie() { return cookie; } /** * Setter for the cookie value. * @param cookie The provided cookie value. */ public void setCookie(MultiDomainServerState cookie) { this.cookie = cookie; } /** * Getter for the serviceId. * @return The serviceId. */ public String getServiceId() { return serviceId; } /** * Getter for the message. * @return The included replication message. */ public UpdateMsg getUpdateMsg() { return updateMsg; } /** * {@inheritDoc} */ @Override public String toString() { return "ECLUpdateMsg:[" + "updateMsg: " + updateMsg + "cookie: " + cookie + "serviceId: " + serviceId + "]"; } /** * {@inheritDoc} */ @Override public byte[] getBytes() throws UnsupportedEncodingException { byte[] byteCookie = String.valueOf(cookie).getBytes("UTF-8"); byte[] byteServiceId = String.valueOf(serviceId).getBytes("UTF-8"); byte[] byteUpdateMsg = updateMsg.getBytes(); int length = 1 + byteCookie.length + 1 + byteServiceId.length + 1 + byteUpdateMsg.length + 1; byte[] resultByteArray = new byte[length]; /* Encode type */ resultByteArray[0] = MSG_TYPE_ECL_UPDATE; int pos = 1; // Encode cookie pos = addByteArray(byteCookie, resultByteArray, pos); // Encode serviceid pos = addByteArray(byteServiceId, resultByteArray, pos); // Encode msg pos = addByteArray(byteUpdateMsg, resultByteArray, pos); return resultByteArray; } } opends/src/server/org/opends/server/replication/protocol/HeartbeatMonitor.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2007-2008 Sun Microsystems, Inc. * Copyright 2007-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.protocol; @@ -103,9 +103,8 @@ boolean gotOneFailure = false; if (debugEnabled()) { TRACER.debugInfo("Heartbeat monitor is starting, expected interval is " + heartbeatInterval + stackTraceToSingleLineString(new Exception())); TRACER.debugInfo(this + " is starting, expected interval is " + heartbeatInterval); } try { opends/src/server/org/opends/server/replication/protocol/ReplServerStartMsg.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Copyright 2006-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.protocol; @@ -167,9 +167,13 @@ pos += length + 1; } /* * read the ServerState */ // Read the ServerState // Caution: ServerState MUST be the last field. Because ServerState can // contain null character (string termination of sererid string ..) it // cannot be decoded using getNextLength() like the other fields. The // only way is to rely on the end of the input buffer : and that forces // the ServerState to be the last. This should be changed and we want to // have more than one ServerState field. serverState = new ServerState(in, pos, in.length - 1); } catch (UnsupportedEncodingException e) { opends/src/server/org/opends/server/replication/protocol/ReplicationMsg.java
@@ -70,6 +70,9 @@ static final byte MSG_TYPE_START_SESSION = 27; static final byte MSG_TYPE_CHANGE_STATUS = 28; static final byte MSG_TYPE_GENERIC_UPDATE = 29; static final byte MSG_TYPE_START_ECL = 30; static final byte MSG_TYPE_START_ECL_SESSION = 31; static final byte MSG_TYPE_ECL_UPDATE = 32; // Adding a new type of message here probably requires to // change accordingly generateMsg method below @@ -215,6 +218,15 @@ case MSG_TYPE_GENERIC_UPDATE: msg = new UpdateMsg(buffer); break; case MSG_TYPE_START_ECL: msg = new ServerStartECLMsg(buffer); break; case MSG_TYPE_START_ECL_SESSION: msg = new StartECLSessionMsg(buffer); break; case MSG_TYPE_ECL_UPDATE: msg = new ECLUpdateMsg(buffer); break; default: throw new DataFormatException("received message with unknown type"); } opends/src/server/org/opends/server/replication/protocol/ServerStartECLMsg.java
New file @@ -0,0 +1,377 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.replication.protocol; import java.io.UnsupportedEncodingException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.zip.DataFormatException; import org.opends.server.replication.common.ServerState; /** * This message is used by LDAP server when they first connect. * to a replication server to let them know who they are and what is their state * (their RUV) */ public class ServerStartECLMsg extends StartMsg { private String serverURL; private int maxReceiveQueue; private int maxSendQueue; private int maxReceiveDelay; private int maxSendDelay; private int windowSize; private ServerState serverState = null; /** * The time in milliseconds between heartbeats from the replication * server. Zero means heartbeats are off. */ private long heartbeatInterval = 0; /** * Whether to continue using SSL to encrypt messages after the start * messages have been exchanged. */ private boolean sslEncryption; /** * Creates a new ServerStartMsg. This message is to be sent by an LDAP * Server after being connected to a replication server for a given * replication domain. * * @param baseDn The base DN. * @param maxReceiveDelay The max receive delay for this server. * @param maxReceiveQueue The max receive Queue for this server. * @param maxSendDelay The max Send Delay from this server. * @param maxSendQueue The max send Queue from this server. * @param windowSize The window size used by this server. * @param heartbeatInterval The requested heartbeat interval. * @param serverState The state of this server. * @param protocolVersion The replication protocol version of the creator. * @param generationId The generationId for this server. * @param sslEncryption Whether to continue using SSL to encrypt messages * after the start messages have been exchanged. * @param groupId The group id of the DS for this DN */ public ServerStartECLMsg(String baseDn, int maxReceiveDelay, int maxReceiveQueue, int maxSendDelay, int maxSendQueue, int windowSize, long heartbeatInterval, ServerState serverState, short protocolVersion, long generationId, boolean sslEncryption, byte groupId) { super(protocolVersion, generationId); this.maxReceiveDelay = maxReceiveDelay; this.maxReceiveQueue = maxReceiveQueue; this.maxSendDelay = maxSendDelay; this.maxSendQueue = maxSendQueue; this.windowSize = windowSize; this.heartbeatInterval = heartbeatInterval; this.sslEncryption = sslEncryption; this.serverState = serverState; this.groupId = groupId; try { /* TODO : find a better way to get the server URL */ this.serverURL = InetAddress.getLocalHost().getHostName(); } catch (UnknownHostException e) { this.serverURL = "Unknown host"; } } /** * Creates a new ServerStartMsg from its encoded form. * * @param in The byte array containing the encoded form of the * ServerStartMsg. * @throws DataFormatException If the byte array does not contain a valid * encoded form of the ServerStartMsg. */ public ServerStartECLMsg(byte[] in) throws DataFormatException { byte[] allowedPduTypes = new byte[1]; allowedPduTypes[0] = MSG_TYPE_START_ECL; headerLength = decodeHeader(allowedPduTypes, in); try { /* first bytes are the header */ int pos = headerLength; /* * read the ServerURL */ int length = getNextLength(in, pos); serverURL = new String(in, pos, length, "UTF-8"); pos += length +1; /* * read the maxReceiveDelay */ length = getNextLength(in, pos); maxReceiveDelay = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; /* * read the maxReceiveQueue */ length = getNextLength(in, pos); maxReceiveQueue = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; /* * read the maxSendDelay */ length = getNextLength(in, pos); maxSendDelay = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; /* * read the maxSendQueue */ length = getNextLength(in, pos); maxSendQueue = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; /* * read the windowSize */ length = getNextLength(in, pos); windowSize = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; /* * read the heartbeatInterval */ length = getNextLength(in, pos); heartbeatInterval = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; /* * read the sslEncryption setting */ length = getNextLength(in, pos); sslEncryption = Boolean.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; // Read the ServerState // Caution: ServerState MUST be the last field. Because ServerState can // contain null character (string termination of sererid string ..) it // cannot be decoded using getNextLength() like the other fields. The // only way is to rely on the end of the input buffer : and that forces // the ServerState to be the last. This should be changed and we want to // have more than one ServerState field. serverState = new ServerState(in, pos, in.length - 1); } catch (UnsupportedEncodingException e) { throw new DataFormatException("UTF-8 is not supported by this jvm."); } } /** * get the Server URL from the message. * @return the server URL */ public String getServerURL() { return serverURL; } /** * Get the maxReceiveDelay. * @return Returns the maxReceiveDelay. */ public int getMaxReceiveDelay() { return maxReceiveDelay; } /** * Get the maxReceiveQueue. * @return Returns the maxReceiveQueue. */ public int getMaxReceiveQueue() { return maxReceiveQueue; } /** * Get the maxSendDelay. * @return Returns the maxSendDelay. */ public int getMaxSendDelay() { return maxSendDelay; } /** * Get the maxSendQueue. * @return Returns the maxSendQueue. */ public int getMaxSendQueue() { return maxSendQueue; } /** * Get the ServerState. * @return The ServerState. */ public ServerState getServerState() { return serverState; } /** * {@inheritDoc} */ @Override public byte[] getBytes() { try { byte[] byteServerUrl = serverURL.getBytes("UTF-8"); byte[] byteMaxRecvDelay = String.valueOf(maxReceiveDelay).getBytes("UTF-8"); byte[] byteMaxRecvQueue = String.valueOf(maxReceiveQueue).getBytes("UTF-8"); byte[] byteMaxSendDelay = String.valueOf(maxSendDelay).getBytes("UTF-8"); byte[] byteMaxSendQueue = String.valueOf(maxSendQueue).getBytes("UTF-8"); byte[] byteWindowSize = String.valueOf(windowSize).getBytes("UTF-8"); byte[] byteHeartbeatInterval = String.valueOf(heartbeatInterval).getBytes("UTF-8"); byte[] byteSSLEncryption = String.valueOf(sslEncryption).getBytes("UTF-8"); byte[] byteServerState = serverState.getBytes(); int length = byteServerUrl.length + 1 + byteMaxRecvDelay.length + 1 + byteMaxRecvQueue.length + 1 + byteMaxSendDelay.length + 1 + byteMaxSendQueue.length + 1 + byteWindowSize.length + 1 + byteHeartbeatInterval.length + 1 + byteSSLEncryption.length + 1 + byteServerState.length + 1; /* encode the header in a byte[] large enough to also contain the mods */ byte resultByteArray[] = encodeHeader(MSG_TYPE_START_ECL, length); int pos = headerLength; pos = addByteArray(byteServerUrl, resultByteArray, pos); pos = addByteArray(byteMaxRecvDelay, resultByteArray, pos); pos = addByteArray(byteMaxRecvQueue, resultByteArray, pos); pos = addByteArray(byteMaxSendDelay, resultByteArray, pos); pos = addByteArray(byteMaxSendQueue, resultByteArray, pos); pos = addByteArray(byteWindowSize, resultByteArray, pos); pos = addByteArray(byteHeartbeatInterval, resultByteArray, pos); pos = addByteArray(byteSSLEncryption, resultByteArray, pos); pos = addByteArray(byteServerState, resultByteArray, pos); return resultByteArray; } catch (UnsupportedEncodingException e) { return null; } } /** * Get the window size for the ldap server that created the message. * * @return The window size for the ldap server that created the message. */ public int getWindowSize() { return windowSize; } /** * Get the heartbeat interval requested by the ldap server that created the * message. * * @return The heartbeat interval requested by the ldap server that created * the message. */ public long getHeartbeatInterval() { return heartbeatInterval; } /** * Get the SSL encryption value for the ldap server that created the * message. * * @return The SSL encryption value for the ldap server that created the * message. */ public boolean getSSLEncryption() { return sslEncryption; } /** * {@inheritDoc} */ @Override public String toString() { return this.getClass().getCanonicalName() + " content: " + "\nprotocolVersion: " + protocolVersion + "\ngenerationId: " + generationId + "\ngroupId: " + groupId + "\nheartbeatInterval: " + heartbeatInterval + "\nmaxReceiveDelay: " + maxReceiveDelay + "\nmaxReceiveQueue: " + maxReceiveQueue + "\nmaxSendDelay: " + maxSendDelay + "\nmaxSendQueue: " + maxSendQueue + "\nserverState: " + serverState + "\nserverURL: " + serverURL + "\nsslEncryption: " + sslEncryption + "\nwindowSize: " + windowSize; } } opends/src/server/org/opends/server/replication/protocol/ServerStartMsg.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Copyright 2006-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.protocol; @@ -208,9 +208,13 @@ sslEncryption = Boolean.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; /* * read the ServerState */ // Read the ServerState // Caution: ServerState MUST be the last field. Because ServerState can // contain null character (string termination of sererid string ..) it // cannot be decoded using getNextLength() like the other fields. The // only way is to rely on the end of the input buffer : and that forces // the ServerState to be the last. This should be changed and we want to // have more than one ServerState field. serverState = new ServerState(in, pos, in.length - 1); } catch (UnsupportedEncodingException e) opends/src/server/org/opends/server/replication/protocol/StartECLSessionMsg.java
New file @@ -0,0 +1,432 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.replication.protocol; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.zip.DataFormatException; import org.opends.server.replication.common.ChangeNumber; /** * This class specifies the parameters of a search request on the ECL. * It is used as an interface between the requestor (plugin part) * - either as an han */ public class StartECLSessionMsg extends ReplicationMsg { /** * This specifies that the ECL is requested from a provided cookie value * defined as a MultiDomainServerState. */ public final static short REQUEST_TYPE_FROM_COOKIE = 0; /** * This specifies that the ECL is requested from a provided interval * of change numbers (as defined by draft-good-ldap-changelog [CHANGELOG] * and NOT replication change numbers). * TODO: not yet implemented */ public final static short REQUEST_TYPE_FROM_DRAFT_CHANGE_NUMBER = 1; /** * This specifies that the ECL is requested ony for the entry that have * a repl change number matching the provided one. * TODO: not yet implemented */ public final static short REQUEST_TYPE_EQUALS_REPL_CHANGE_NUMBER = 2; /** * This specifies that the request on the ECL is a PERSISTENT search * with changesOnly = false. */ public final static short PERSISTENT = 0; /** * This specifies that the request on the ECL is a NOT a PERSISTENT search. */ public final static short NON_PERSISTENT = 1; /** * This specifies that the request on the ECL is a PERSISTENT search * with changesOnly = false. */ public final static short PERSISTENT_CHANGES_ONLY = 2;; // The type of request as defined by REQUEST_TYPE_... private short eclRequestType; // When eclRequestType = FROM_COOKIE, // specifies the provided cookie value. private String crossDomainServerState = ""; // When eclRequestType = FROM_CHANGE_NUMBER, // specifies the provided change number first and last - [CHANGELOG] private int firstDraftChangeNumber = -1; private int lastDraftChangeNumber = -1; // When eclRequestType = EQUALS_REPL_CHANGE_NUMBER, // specifies the provided replication change number. private ChangeNumber changeNumber; // Specifies whether the search is persistent and changesOnly private short isPersistent; // A string helping debuging and tracing the client operation related when // processing, on the RS side, a request on the ECL. private String operationId = ""; // Excluded domains private ArrayList<String> excludedServiceIDs = new ArrayList<String>(); /** * Creates a new StartSessionMsg message from its encoded form. * * @param in The byte array containing the encoded form of the message. * @throws java.util.zip.DataFormatException If the byte array does not * contain a valid encoded form of the message. */ public StartECLSessionMsg(byte[] in) throws DataFormatException { /* * The message is stored in the form: * <message type><status><assured flag><assured mode><safe data level> * <list of referrals urls> * (each referral url terminates with 0) */ try { /* first bytes are the header */ int pos = 0; /* first byte is the type */ if (in.length < 1 || in[pos++] != MSG_TYPE_START_ECL_SESSION) { throw new DataFormatException( "Input is not a valid " + this.getClass().getCanonicalName()); } // start mode int length = getNextLength(in, pos); eclRequestType = Short.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; // sequenceNumber length = getNextLength(in, pos); firstDraftChangeNumber = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; // stopSequenceNumber length = getNextLength(in, pos); lastDraftChangeNumber = Integer.valueOf(new String(in, pos, length, "UTF-8")); pos += length +1; // replication changeNumber length = getNextLength(in, pos); String changenumberStr = new String(in, pos, length, "UTF-8"); pos += length + 1; changeNumber = new ChangeNumber(changenumberStr); // persistentSearch mode length = getNextLength(in, pos); isPersistent = Short.valueOf(new String(in, pos, length, "UTF-8")); pos += length + 1; // generalized state length = getNextLength(in, pos); crossDomainServerState = new String(in, pos, length, "UTF-8"); pos += length + 1; // operation id length = getNextLength(in, pos); operationId = new String(in, pos, length, "UTF-8"); pos += length + 1; // excluded DN length = getNextLength(in, pos); String excludedDNsString = new String(in, pos, length, "UTF-8"); if (excludedDNsString.length()>0) { String[] excludedDNsStr = excludedDNsString.split(";"); for (String excludedDNStr : excludedDNsStr) { this.excludedServiceIDs.add(excludedDNStr); } } pos += length + 1; } catch (UnsupportedEncodingException e) { throw new DataFormatException("UTF-8 is not supported by this jvm."); } catch (IllegalArgumentException e) { throw new DataFormatException(e.getMessage()); } } /** * Creates a new StartSessionMsg message with the given required parameters. */ public StartECLSessionMsg() { changeNumber = new ChangeNumber((short)0,0,(short)0); } /** * {@inheritDoc} */ @Override public byte[] getBytes() { String excludedSIDsString = new String(); for (String excludedServiceID : excludedServiceIDs) { excludedSIDsString = excludedSIDsString.concat(excludedServiceID+";"); } try { byte[] byteMode = Short.toString(eclRequestType).getBytes("UTF-8"); byte[] byteSequenceNumber = String.valueOf(firstDraftChangeNumber).getBytes("UTF-8"); byte[] byteStopSequenceNumber = String.valueOf(lastDraftChangeNumber).getBytes("UTF-8"); byte[] byteChangeNumber = changeNumber.toString().getBytes("UTF-8"); byte[] bytePsearch = Short.toString(isPersistent).getBytes(); byte[] byteGeneralizedState = String.valueOf(crossDomainServerState).getBytes("UTF-8"); byte[] byteOperationId = String.valueOf(operationId).getBytes("UTF-8"); byte[] byteExcludedDNs = String.valueOf(excludedSIDsString).getBytes("UTF-8"); int length = byteMode.length + 1 + byteSequenceNumber.length + 1 + byteStopSequenceNumber.length + 1 + byteChangeNumber.length + 1 + bytePsearch.length + 1 + byteGeneralizedState.length + 1 + byteOperationId.length + 1 + byteExcludedDNs.length + 1 + 1; byte[] resultByteArray = new byte[length]; int pos = 0; resultByteArray[pos++] = MSG_TYPE_START_ECL_SESSION; pos = addByteArray(byteMode, resultByteArray, pos); pos = addByteArray(byteSequenceNumber, resultByteArray, pos); pos = addByteArray(byteStopSequenceNumber, resultByteArray, pos); pos = addByteArray(byteChangeNumber, resultByteArray, pos); pos = addByteArray(bytePsearch, resultByteArray, pos); pos = addByteArray(byteGeneralizedState, resultByteArray, pos); pos = addByteArray(byteOperationId, resultByteArray, pos); pos = addByteArray(byteExcludedDNs, resultByteArray, pos); return resultByteArray; } catch (IOException e) { // never happens return null; } } /** * {@inheritDoc} */ @Override public String toString() { return (this.getClass().getCanonicalName() + " [" + " requestType="+ eclRequestType + " persistentSearch=" + isPersistent + " changeNumber=" + changeNumber + " firstDraftChangeNumber=" + firstDraftChangeNumber + " lastDraftChangeNumber=" + lastDraftChangeNumber + " generalizedState=" + crossDomainServerState + " operationId=" + operationId + " excludedDNs=" + excludedServiceIDs + "]"); } /** * Getter on the changer number start. * @return the changer number start. */ public int getFirstDraftChangeNumber() { return firstDraftChangeNumber; } /** * Getter on the changer number stop. * @return the change number stop. */ public int getLastDraftChangeNumber() { return lastDraftChangeNumber; } /** * Setter on the first changer number (as defined by [CHANGELOG]). * @param firstDraftChangeNumber the provided first change number. */ public void setFirstDraftChangeNumber(int firstDraftChangeNumber) { this.firstDraftChangeNumber = firstDraftChangeNumber; } /** * Setter on the last changer number (as defined by [CHANGELOG]). * @param lastDraftChangeNumber the provided last change number. */ public void setLastDraftChangeNumber(int lastDraftChangeNumber) { this.lastDraftChangeNumber = lastDraftChangeNumber; } /** * Getter on the replication change number. * @return the replication change number. */ public ChangeNumber getChangeNumber() { return changeNumber; } /** * Setter on the replication change number. * @param changeNumber the provided replication change number. */ public void setChangeNumber(ChangeNumber changeNumber) { this.changeNumber = changeNumber; } /** * Getter on the type of request. * @return the type of request. */ public short getECLRequestType() { return eclRequestType; } /** * Setter on the type of request. * @param eclRequestType the provided type of request. */ public void setECLRequestType(short eclRequestType) { this.eclRequestType = eclRequestType; } /** * Getter on the persistent property of the search request on the ECL. * @return the persistent property. */ public short isPersistent() { return this.isPersistent; } /** * Setter on the persistent property of the search request on the ECL. * @param isPersistent the provided persistent property. */ public void setPersistent(short isPersistent) { this.isPersistent = isPersistent; } /** * Getter of the cross domain server state. * @return the cross domain server state. */ public String getCrossDomainServerState() { return this.crossDomainServerState; } /** * Setter of the cross domain server state. * @param crossDomainServerState the provided cross domain server state. */ public void setCrossDomainServerState(String crossDomainServerState) { this.crossDomainServerState = crossDomainServerState; } /** * Setter of the operation id. * @param operationId The provided opration id. */ public void setOperationId(String operationId) { this.operationId = operationId; } /** * Getter on the operation id. * @return the operation id. */ public String getOperationId() { return this.operationId; } /** * Getter on the list of excluded ServiceIDs. * @return the list of excluded ServiceIDs. */ public ArrayList<String> getExcludedServiceIDs() { return this.excludedServiceIDs; } /** * Setter on the list of excluded ServiceIDs. * @param excludedServiceIDs the provided list of excluded ServiceIDs. */ public void setExcludedDNs(ArrayList<String> excludedServiceIDs) { this.excludedServiceIDs = excludedServiceIDs; } } opends/src/server/org/opends/server/replication/server/DataServerHandler.java
New file @@ -0,0 +1,724 @@ /* * 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-2009 Sun Microsystems, Inc. */ 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.util.StaticUtils.stackTraceToSingleLineString; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; 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 { // 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; /** * 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( ProtocolSession session, int queueSize, String replicationServerURL, short 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 = null; 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. try { if (session != null) session.close(); } catch (IOException e) { // ignore } // 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( Short.toString(replicationServerDomain. getReplicationServer().getServerId()), getServiceId().toString(), Short.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().toString(), Short.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().toString(), Short.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())); try { MonitorData md; md = replicationServerDomain.computeMonitorData(); // 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()); } } catch (Exception e) { Message message = ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e)); // We failed retrieving the monitor data. attributes.add(Attributes.create("error", message.toString())); } return attributes; } /** * {@inheritDoc} */ @Override public String getMonitorInstanceName() { String str = serverURL + " " + String.valueOf(serverId); return "Connected Replica " + str + ",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().toString(), Short.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().toString(), Short.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 { generationId = serverStartMsg.getGenerationId(); protocolVersion = ProtocolVersion.minWithCurrent( serverStartMsg.getVersion()); serverId = serverStartMsg.getServerId(); serverURL = serverStartMsg.getServerURL(); groupId = serverStartMsg.getGroupId(); maxReceiveDelay = serverStartMsg.getMaxReceiveDelay(); maxReceiveQueue = serverStartMsg.getMaxReceiveQueue(); maxSendDelay = serverStartMsg.getMaxSendDelay(); maxSendQueue = serverStartMsg.getMaxSendQueue(); heartbeatInterval = serverStartMsg.getHeartbeatInterval(); // generic stuff setServiceIdAndDomain(serverStartMsg.getBaseDn()); setInitialServerState(serverStartMsg.getServerState()); setSendWindowSize(serverStartMsg.getWindowSize()); if (maxReceiveQueue > 0) restartReceiveQueue = (maxReceiveQueue > 1000 ? maxReceiveQueue - 200 : maxReceiveQueue * 8 / 10); else restartReceiveQueue = 0; if (maxSendQueue > 0) restartSendQueue = (maxSendQueue > 1000 ? maxSendQueue - 200 : maxSendQueue * 8 / 10); else restartSendQueue = 0; if (maxReceiveDelay > 0) restartReceiveDelay = (maxReceiveDelay > 10 ? maxReceiveDelay - 1 : maxReceiveDelay); else restartReceiveDelay = 0; if (maxSendDelay > 0) restartSendDelay = (maxSendDelay > 10 ? maxSendDelay - 1 : maxSendDelay); else restartSendDelay = 0; 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() { // Alright, connected with new DS: store handler. Map<Short, DataServerHandler> connectedDSs = replicationServerDomain.getConnectedDSs(); connectedDSs.put(serverId, this); // Tell peer DSs a new DS just connected to us // No need to resend topo msg 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); session.publish(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); // Get or Create the ReplicationServerDomain replicationServerDomain = getDomain(true); localGenerationId = replicationServerDomain.getGenerationId(); oldGenerationId = localGenerationId; // 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 replicationServerDomain. waitDisconnection(inServerStartMsg.getServerId()); // Duplicate server ? if (!replicationServerDomain.checkForDuplicateDS(this)) { abortStart(null); return; } // lock with no timeout lockDomain(false); // ReplServerStartMsg outReplServerStartMsg = null; try { outReplServerStartMsg = sendStartToRemote((short)-1); // log logStartHandshakeRCVandSND(inServerStartMsg, outReplServerStartMsg); // The session initiator decides whether to use SSL. // Until here session is encrypted then it depends on the negociation if (!sessionInitiatorSSLEncryption) session.stopEncryption(); // wait and process StartSessionMsg from remote RS StartSessionMsg inStartSessionMsg = waitAndProcessStartSessionFromRemoteDS(); // Send our own TopologyMsg to remote RS TopologyMsg outTopoMsg = sendTopoToRemoteDS(); logStartSessionHandshake(inStartSessionMsg, outTopoMsg); } catch(IOException e) { // We do not want polluting error log if error is due to normal session // aborted after handshake phase one from a DS that is searching for // best suitable RS. // don't log a poluting error when connection aborted // from a DS that wanted only to perform handshake phase 1 in order // to determine the best suitable RS: // 1) -> ServerStartMsg // 2) <- ReplServerStartMsg // 3) connection closure throw new DirectoryException(ResultCode.OTHER, null, null); } 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(); registerIntoDomain(); super.finalizeStart(); } catch(DirectoryException de) { abortStart(de.getMessageObject()); } catch(Exception e) { abortStart(null); } finally { if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); } } /** * Creates a DSInfo structure representing this remote DS. * @return The DSInfo structure representing this remote DS */ public DSInfo toDSInfo() { DSInfo dsInfo = new DSInfo(serverId, replicationServerId, generationId, status, assuredFlag, assuredMode, safeDataLevel, groupId, refUrls); return dsInfo; } /** * {@inheritDoc} */ @Override public String toString() { String localString; if (serverId != 0) { localString = "Directory Server "; localString += serverId + " " + serverURL + " " + getServiceId(); } else localString = "Unknown server"; return localString; } /** * Wait receiving the StartSessionMsg from the remote DS and process it. * @return the startSessionMsg received * @throws DirectoryException * @throws IOException * @throws ClassNotFoundException * @throws DataFormatException * @throws NotSupportedOldVersionPDUException */ private StartSessionMsg waitAndProcessStartSessionFromRemoteDS() throws DirectoryException, IOException, ClassNotFoundException, DataFormatException, NotSupportedOldVersionPDUException { ReplicationMsg msg = null; msg = session.receive(); if (!(msg instanceof StartSessionMsg)) { Message message = Message.raw( "Protocol error: StartSessionMsg required." + msg + " received."); abortStart(message); } // 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().toString(), Short.toString(serverId)); throw new DirectoryException(ResultCode.OTHER, message); } this.refUrls = startSessionMsg.getReferralsURLs(); this.assuredFlag = startSessionMsg.isAssured(); this.assuredMode = startSessionMsg.getAssuredMode(); this.safeDataLevel = startSessionMsg.getSafeDataLevel(); /* * 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) */ if (localGenerationId > 0) { if (generationId != localGenerationId) { Message message = NOTE_BAD_GENERATION_ID_FROM_DS.get( getServiceId(), Short.toString(serverId), Long.toString(generationId), Long.toString(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 = NOTE_BAD_GENERATION_ID_FROM_DS.get( getServiceId(), Short.toString(serverId), Long.toString(generationId), Long.toString(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.setGenerationId(generationId, false); } } return startSessionMsg; } } opends/src/server/org/opends/server/replication/server/ECLServerHandler.java
New file @@ -0,0 +1,1551 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.replication.server; import static org.opends.messages.ReplicationMessages.*; import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.zip.DataFormatException; import org.opends.messages.Category; import org.opends.messages.Message; import org.opends.messages.Severity; import org.opends.server.api.DirectoryThread; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.MultiDomainServerState; import org.opends.server.replication.common.ServerState; import org.opends.server.replication.common.ServerStatus; import org.opends.server.replication.protocol.*; import org.opends.server.types.Attribute; import org.opends.server.types.Attributes; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.ResultCode; import org.opends.server.util.TimeThread; /** * This class defines a server handler, which handles all interaction with a * peer replication server. */ public class ECLServerHandler extends ServerHandler { // Properties filled only if remote server is a RS private String serverAddressURL; String operationId; /** * CLDomainContext : contains the state properties for the search * currently being processed, by replication domain. */ private class CLDomainContext { ReplicationServerDomain rsd; // the repl server domain boolean active; // is the domain still active MessageHandler mh; // the message handler associated UpdateMsg nextMsg; UpdateMsg nonElligiblemsg; ServerState startState; ServerState currentState; ServerState stopState; /** * Add to the provider buffer a string representation of this object. */ public void toString(StringBuilder buffer, int i) { CLDomainContext xx = clDomCtxts[i]; buffer.append( " clDomCtxts(" + i + ") [act=" + xx.active + " rsd=" + rsd + " nextMsg=" + nextMsg + "(" + (nextMsg != null? new Date(nextMsg.getChangeNumber().getTime()).toString():"") + ")" + " nextNonEligibleMsg=" + nonElligiblemsg + " startState=" + startState + " stopState= " + stopState + " currState= " + currentState + "]"); } } // The list of contexts by domain for the current search CLDomainContext[] clDomCtxts = new CLDomainContext[0]; private void clDomCtxtsToString(String msg) { StringBuilder buffer = new StringBuilder(); buffer.append(msg+"\n"); for (int i=0;i<clDomCtxts.length;i++) { clDomCtxts[i].toString(buffer, i); buffer.append("\n"); } TRACER.debugInfo( "In " + this.getName() + " clDomCtxts: " + buffer.toString()); } /** * Class that manages the state variables for the current search on the ECL. */ private class CLTraverseCtxt { /** * Specifies the next changer number (seqnum), -1 when not. */ public int nextSeqnum; /** * Specifies whether the current search has been requested to be persistent * or not. */ public short isPersistent; /** * Specifies the last changer number (seqnum) requested. */ public int stopSeqnum; /** * Specifies whether the change number (seqnum) db has been read until * its end. */ public boolean endOfSeqnumdbReached = false; /** * Specifies the current search phase. * 1 = init * 2 = persistent */ public int searchPhase = 1; /** * Specifies the cookie contained in the request, specifying where * to start serving the ECL. */ public String generalizedStartState; /** * Specifies the current cookie value. */ public MultiDomainServerState currentCookie = new MultiDomainServerState(); /** * Specifies the excluded DNs. */ public ArrayList<String> excludedServiceIDs = new ArrayList<String>(); /** * Provides a string representation of this object. * @return the string representation. */ public String toString() { return new String( this.getClass().getCanonicalName() + ":[" + " nextSeqnum=" + nextSeqnum + " persistent=" + isPersistent + " stopSeqnum" + stopSeqnum + " endOfSeqnumdbReached=" + endOfSeqnumdbReached + " searchPhase=" + searchPhase + " generalizedStartState=" + generalizedStartState + "]"); } } // The context of the current search private CLTraverseCtxt cLSearchCtxt = new CLTraverseCtxt(); /** * Starts this handler based on a start message received from remote server. * @param inECLStartMsg The start msg provided by the remote server. * @return Whether the remote server requires encryption or not. * @throws DirectoryException When a problem occurs. */ public boolean processStartFromRemote(ServerStartECLMsg inECLStartMsg) throws DirectoryException { try { protocolVersion = ProtocolVersion.minWithCurrent( inECLStartMsg.getVersion()); generationId = inECLStartMsg.getGenerationId(); serverURL = inECLStartMsg.getServerURL(); int separator = serverURL.lastIndexOf(':'); serverAddressURL = session.getRemoteAddress() + ":" + serverURL.substring(separator + 1); setInitialServerState(inECLStartMsg.getServerState()); setSendWindowSize(inECLStartMsg.getWindowSize()); if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { // We support connection from a V1 RS // Only V2 protocol has the group id in repl server start message this.groupId = inECLStartMsg.getGroupId(); } // FIXME:ECL Any generationID must be removed, it makes no sense here. oldGenerationId = -100; } catch(Exception e) { Message message = Message.raw(e.getLocalizedMessage()); throw new DirectoryException(ResultCode.OTHER, message); } return inECLStartMsg.getSSLEncryption(); } /** * Creates a new handler object to a remote replication server. * @param session The session with the remote RS. * @param queueSize The queue size to manage updates to that RS. * @param replicationServerURL The hosting local RS URL. * @param replicationServerId The hosting local RS serverId. * @param replicationServer The hosting local RS object. * @param rcvWindowSize The receiving window size. */ public ECLServerHandler( ProtocolSession session, int queueSize, String replicationServerURL, short replicationServerId, ReplicationServer replicationServer, int rcvWindowSize) { super(session, queueSize, replicationServerURL, replicationServerId, replicationServer, rcvWindowSize); try { setServiceIdAndDomain("cn=changelog"); } catch(DirectoryException de) { // no chance to have a bad domain set here } } /** * Creates a new handler object to a remote replication server. * @param replicationServerURL The hosting local RS URL. * @param replicationServerId The hosting local RS serverId. * @param replicationServer The hosting local RS object. * @param startECLSessionMsg the start parameters. * @throws DirectoryException when an errors occurs. */ public ECLServerHandler( String replicationServerURL, short replicationServerId, ReplicationServer replicationServer, StartECLSessionMsg startECLSessionMsg) throws DirectoryException { // FIXME:ECL queueSize is hard coded to 1 else Handler hangs for some reason super(null, 1, replicationServerURL, replicationServerId, replicationServer, 0); try { setServiceIdAndDomain("cn=changelog"); } catch(DirectoryException de) { // no chance to have a bad domain set here } this.initialize(startECLSessionMsg); } /** * Starts the handler from a remote start message received from * the remote server. * @param inECLStartMsg The provided ReplServerStart message received. */ public void startFromRemoteServer(ServerStartECLMsg inECLStartMsg) { try { // Process start from remote boolean sessionInitiatorSSLEncryption = processStartFromRemote(inECLStartMsg); // lock with timeout lockDomain(true); // send start to remote ReplServerStartMsg outReplServerStartMsg = sendStartToRemote((short)-1); // log logStartHandshakeRCVandSND(inECLStartMsg, outReplServerStartMsg); // until here session is encrypted then it depends on the negociation // The session initiator decides whether to use SSL. if (!sessionInitiatorSSLEncryption) session.stopEncryption(); // wait and process StartSessionMsg from remote RS StartECLSessionMsg inStartECLSessionMsg = waitAndProcessStartSessionECLFromRemoteServer(); logStartECLSessionHandshake(inStartECLSessionMsg); // initialization initialize(inStartECLSessionMsg); } catch(DirectoryException de) { abortStart(de.getMessageObject()); } catch(Exception e) { abortStart(Message.raw(e.getLocalizedMessage())); } finally { if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) { replicationServerDomain.release(); } } } /** * Wait receiving the StartSessionMsg from the remote DS and process it. * @return the startSessionMsg received * @throws DirectoryException * @throws IOException * @throws ClassNotFoundException * @throws DataFormatException * @throws NotSupportedOldVersionPDUException */ private StartECLSessionMsg waitAndProcessStartSessionECLFromRemoteServer() throws DirectoryException, IOException, ClassNotFoundException, DataFormatException, NotSupportedOldVersionPDUException { ReplicationMsg msg = null; msg = session.receive(); if (!(msg instanceof StartECLSessionMsg)) { Message message = Message.raw( "Protocol error: StartECLSessionMsg required." + msg + " received."); abortStart(message); } // Process StartSessionMsg sent by remote DS StartECLSessionMsg startECLSessionMsg = (StartECLSessionMsg) msg; return startECLSessionMsg; } /** * Initialize the handler from a provided cookie value. * @param providedGeneralizedStartState The provided cookie value. * @throws DirectoryException When an error is raised. */ public void initializeCLSearchFromGenState( String providedGeneralizedStartState) throws DirectoryException { this.cLSearchCtxt.nextSeqnum = -1; // will not generate seqnum initializeCLDomCtxts(providedGeneralizedStartState); } /** * Initialize the context for each domain. * @param providedGeneralizedStartState the provided generalized state * @throws DirectoryException When an error occurs. */ public void initializeCLDomCtxts(String providedGeneralizedStartState) throws DirectoryException { HashMap<String,ServerState> startStates = new HashMap<String,ServerState>(); ReplicationServer rs = replicationServerDomain.getReplicationServer(); try { // Initialize start state for all running domains with empty state Iterator<ReplicationServerDomain> rsdk = rs.getCacheIterator(); if (rsdk != null) { while (rsdk.hasNext()) { // process a domain ReplicationServerDomain rsd = rsdk.next(); // skip the changelog domain if (rsd == this.replicationServerDomain) continue; startStates.put(rsd.getBaseDn(), new ServerState()); } } // Overwrite start state from the cookie provided in the request if ((providedGeneralizedStartState != null) && (providedGeneralizedStartState.length()>0)) { String[] domains = providedGeneralizedStartState.split(";"); for (String domainState : domains) { // Split baseDN and serverState String[] fields = domainState.split(":"); // BaseDN - Check it String domainBaseDNReceived = fields[0]; if (!startStates.containsKey(domainBaseDNReceived)) throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED.get( "unknown " + domainBaseDNReceived)); // ServerState ServerState domainServerState = new ServerState(); if (fields.length>1) { String strState = fields[1]; String[] strCN = strState.split(" "); for (String sr : strCN) { ChangeNumber fromChangeNumber = new ChangeNumber(sr); domainServerState.update(fromChangeNumber); } } startStates.put(domainBaseDNReceived, domainServerState); // FIXME: ECL first cookie value check // ECL For each of the provided state, it this state is older // than the older change stored in the replication changelog .... // then a purge occured since the time the cookie was published // it is recommended to do a full resync ReplicationServerDomain rsd = rs.getReplicationServerDomain(domainBaseDNReceived, false); ServerState domainStartState = rsd.getStartState(); if (!domainServerState.cover(domainStartState)) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED.get( "too old cookie provided " + providedGeneralizedStartState + " first acceptable change for " + rsd.getBaseDn() + " is " + rsd.getStartState())); } } } } catch(DirectoryException de) { throw de; } catch(Exception e) { throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_INVALID_COOKIE_FULL_RESYNC_REQUIRED.get( "Exception raised: " + e.getMessage())); } try { // Now traverse all domains and build the initial changelog context Iterator<ReplicationServerDomain> rsdi = rs.getCacheIterator(); // Creates the table that will contain the real-time info by domain. clDomCtxts = new CLDomainContext[rs.getCacheSize()-1 -this.cLSearchCtxt.excludedServiceIDs.size()]; int i =0; if (rsdi != null) { while (rsdi.hasNext()) { // process a domain ReplicationServerDomain rsd = rsdi.next(); // skip the 'unreal' changelog domain if (rsd == this.replicationServerDomain) continue; // skip the excluded domains boolean excluded = false; for(String excludedServiceID : this.cLSearchCtxt.excludedServiceIDs) { if (excludedServiceID.equalsIgnoreCase(rsd.getBaseDn())) { excluded=true; break; } } if (excluded) continue; // Creates the context record CLDomainContext newContext = new CLDomainContext(); newContext.active = true; newContext.rsd = rsd; if (this.cLSearchCtxt.isPersistent == StartECLSessionMsg.PERSISTENT_CHANGES_ONLY) { newContext.startState = rsd.getCLElligibleState(); } else { newContext.startState = startStates.get(rsd.getBaseDn()); newContext.stopState = rsd.getCLElligibleState(); } newContext.currentState = new ServerState(); // Creates an unconnected SH MessageHandler mh = new MessageHandler(maxQueueSize, replicationServerURL, replicationServerId, replicationServer); // set initial state mh.setInitialServerState(newContext.startState); // set serviceID and domain mh.setServiceIdAndDomain(rsd.getBaseDn()); // register into domain rsd.registerHandler(mh); newContext.mh = mh; // store the new context clDomCtxts[i] = newContext; i++; } } // the next record from the seqnumdb should be the one cLSearchCtxt.endOfSeqnumdbReached = false; cLSearchCtxt.generalizedStartState = providedGeneralizedStartState; // Initializes all domain with the next elligible message for (int j=0; j<clDomCtxts.length; j++) { this.getNextElligibleMessage(j); if (clDomCtxts[j].nextMsg == null) clDomCtxts[j].active = false; } } catch(Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); throw new DirectoryException( ResultCode.OPERATIONS_ERROR, Message.raw(Category.SYNC, Severity.INFORMATION,"Exception raised: " + e.getLocalizedMessage()), e); } } /** * Registers this handler into its related domain and notifies the domain. */ private void registerIntoDomain() { replicationServerDomain.registerHandler(this); } /** * Shutdown this handler ServerHandler. */ public void shutdown() { for (int i=0;i<clDomCtxts.length;i++) { if (!clDomCtxts[i].rsd.unRegisterHandler(clDomCtxts[i].mh)) { TRACER.debugInfo(this +" shutdown() Internal error " + " when unregistering "+ clDomCtxts[i].mh); } clDomCtxts[i].rsd.stopServer(clDomCtxts[i].mh); } super.shutdown(); clDomCtxts = null; } /** * Request to shutdown the associated writer. */ protected void shutdownWriter() { shutdownWriter = true; if (writer!=null) { ECLServerWriter eclWriter = (ECLServerWriter)this.writer; eclWriter.shutdownWriter(); } } /** * {@inheritDoc} */ @Override public String getMonitorInstanceName() { String str = serverURL + " " + String.valueOf(serverId); return "Connected External Changelog Server " + str + ",cn=" + replicationServerDomain.getMonitorInstanceName(); } /** * 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 RS ones attributes.add(Attributes.create("External-Changelog-Server", serverURL)); try { MonitorData md; md = replicationServerDomain.computeMonitorData(); // FIXME:ECL No monitoring exist for ECL. } catch (Exception e) { Message message = ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e)); // We failed retrieving the monitor data. attributes.add(Attributes.create("error", message.toString())); } return attributes; } /** * {@inheritDoc} */ @Override public String toString() { String localString; localString = "External changelog Server "; if (this.cLSearchCtxt==null) localString += serverId + " " + serverURL + " " + getServiceId(); else localString += this.getName(); return localString; } /** * Gets the status of the connected DS. * @return The status of the connected DS. */ public ServerStatus getStatus() { // FIXME:ECL Sould ECLServerHandler manage a ServerStatus ? return ServerStatus.INVALID_STATUS; } /** * Retrieves the Address URL for this server handler. * * @return The Address URL for this server handler, * in the form of an IP address and port separated by a colon. */ public String getServerAddressURL() { return serverAddressURL; } /** * {@inheritDoc} */ @Override public boolean isDataServer() { return true; } /** * Initialize the handler. * @param startECLSessionMsg The provided starting state. * @throws DirectoryException when a problem occurs. */ public void initialize(StartECLSessionMsg startECLSessionMsg) throws DirectoryException { // this.following = false; // FIXME:ECL makes no sense for ECLServerHandler ? this.lateQueue.clear(); // FIXME:ECL makes no sense for ECLServerHandler ? this.setConsumerActive(true); this.cLSearchCtxt.searchPhase = 1; this.operationId = startECLSessionMsg.getOperationId(); this.setName(this.getClass().getCanonicalName()+ " " + operationId); if (eligibleCNComputerThread==null) eligibleCNComputerThread = new ECLEligibleCNComputerThread(); cLSearchCtxt.isPersistent = startECLSessionMsg.isPersistent(); cLSearchCtxt.stopSeqnum = startECLSessionMsg.getLastDraftChangeNumber(); cLSearchCtxt.searchPhase = 1; cLSearchCtxt.currentCookie = new MultiDomainServerState( startECLSessionMsg.getCrossDomainServerState()); cLSearchCtxt.excludedServiceIDs=startECLSessionMsg.getExcludedServiceIDs(); //-- if (startECLSessionMsg.getECLRequestType()==0) { initializeCLSearchFromGenState( startECLSessionMsg.getCrossDomainServerState()); } if (session != null) { try { // Disable timeout for next communications session.setSoTimeout(0); } catch(Exception e) {} // sendWindow MUST be created before starting the writer sendWindow = new Semaphore(sendWindowSize); // create reader reader = new ServerReader(session, serverId, this, replicationServerDomain); reader.start(); if (writer == null) { // create writer writer = new ECLServerWriter(session,this,replicationServerDomain); writer.start(); } // Resume the writer ((ECLServerWriter)writer).resumeWriter(); // FIXME:ECL Potential race condition if writer not yet resumed here } if (cLSearchCtxt.isPersistent == StartECLSessionMsg.PERSISTENT_CHANGES_ONLY) { closePhase1(); } /* TODO: Good Draft Compat //-- if (startCLMsg.getStartMode()==1) { initializeCLSearchFromProvidedSeqnum(startCLMsg.getSequenceNumber()); } //-- if (startCLMsg.getStartMode()==2) { if (CLSearchFromProvidedExactCN(startCLMsg.getChangeNumber())) return; } //-- if (startCLMsg.getStartMode()==4) { // to get the CL first and last initializeCLDomCtxts(null); // from start ChangeNumber crossDomainElligibleCN = computeCrossDomainElligibleCN(); try { // to get the CL first and last // last rely on the crossDomainElligibleCN thhus must have been // computed before int[] limits = computeCLLimits(crossDomainElligibleCN); // Send the response CLLimitsMsg msg = new CLLimitsMsg(limits[0], limits[1]); session.publish(msg); } catch(Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); try { session.publish( new ErrorMsg( replicationServerDomain.getReplicationServer().getServerId(), serverId, Message.raw(Category.SYNC, Severity.INFORMATION, "Exception raised: " + e.getMessage()))); } catch(IOException ioe) { // FIXME: close conn ? } } return; } Good Draft Compat */ // Store into domain registerIntoDomain(); } /** * Select the next update that must be sent to the server managed by this * ServerHandler. * * @return the next update that must be sent to the server managed by this * ServerHandler. * @exception DirectoryException when an error occurs. */ public ECLUpdateMsg takeECLUpdate() throws DirectoryException { boolean interrupted = true; ECLUpdateMsg msg = getnextUpdate(); // FIXME:ECL We should refactor so that a SH always have a session if (session == null) return msg; /* * When we remove a message from the queue we need to check if another * server is waiting in flow control because this queue was too long. * This check might cause a performance penalty an therefore it * is not done for every message removed but only every few messages. */ /** FIXME:ECL checkAllSaturation makes no sense for ECLServerHandler ? if (++saturationCount > 10) { saturationCount = 0; try { replicationServerDomain.checkAllSaturation(); } catch (IOException e) { } } */ boolean acquired = false; do { try { acquired = sendWindow.tryAcquire((long) 500, TimeUnit.MILLISECONDS); interrupted = false; } catch (InterruptedException e) { // loop until not interrupted } } while (((interrupted) || (!acquired)) && (!shutdownWriter)); if (msg != null) { incrementOutCount(); } return msg; } /** * Get the next message - non blocking - null when none. * * @param synchronous - not used - always non blocking. * @return the next message - null when none. */ protected UpdateMsg getnextMessage(boolean synchronous) { UpdateMsg msg = null; try { ECLUpdateMsg eclMsg = getnextUpdate(); if (eclMsg!=null) msg = eclMsg.getUpdateMsg(); } catch(DirectoryException de) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } return msg; } /** * Get the next external changelog update. * * @return The ECL update, null when none. * @exception DirectoryException when any problem occurs. */ protected ECLUpdateMsg getnextUpdate() throws DirectoryException { return getGeneralizedNextECLUpdate(this.cLSearchCtxt); } /** * Computes the cross domain eligible message (non blocking). * Return null when search is covered */ private ECLUpdateMsg getGeneralizedNextECLUpdate(CLTraverseCtxt cLSearchCtxt) throws DirectoryException { ECLUpdateMsg theOldestChange = null; try { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + "," + this + " getGeneralizedNextECLUpdate starts with ctxt=" + cLSearchCtxt); if ((cLSearchCtxt.nextSeqnum != -1) && (!cLSearchCtxt.endOfSeqnumdbReached)) { /* TODO:ECL G Good changelog draft compat. // First time , initialise the cursor to traverse the seqnumdb if (seqnumDbReadIterator == null) { try { seqnumDbReadIterator = replicationServerDomain.getReplicationServer(). getSeqnumDbHandler().generateIterator(cLSearchCtxt.nextSeqnum); TRACER.debugInfo("getGeneralizedNextMessage(): " + " creates seqnumDbReadIterator from nextSeqnum=" + cLSearchCtxt.nextSeqnum + " 1rst=" + seqnumDbReadIterator.getSeqnum() + " CN=" + seqnumDbReadIterator.getChangeNumber() + cLSearchCtxt); } catch(Exception e) { cLSearchCtxt.endOfSeqnumdbReached = true; } } */ } // Search / no seqnum / not persistent // ----------------------------------- // init: all domain are candidate // get one msg from each // no (null) msg returned: should not happen since we go to a state // that is computed/expected // getMessage: // get the oldest msg: // after: // if stopState of domain is covered then domain is out candidate // until no domain candidate mean generalized stopState // has been reached // else // get one msg from that domain // no (null) msg returned: should not happen since we go to a state // that is computed/expected // step 2: send DoneMsg // Persistent: // ---------- // step 1&2: same as non persistent // // step 3: reinit all domain are candidate // take the oldest // if one domain has no msg, still is candidate int iDom = 0; boolean nextclchange = true; while ((nextclchange) && (cLSearchCtxt.searchPhase==1)) { // Step 1 & 2 if (cLSearchCtxt.searchPhase==1) { if (debugEnabled()) clDomCtxtsToString("In getGeneralizedNextMessage : " + "looking for the generalized oldest change"); // Retrieves the index in the subx table of the // generalizedOldestChange iDom = getGeneralizedOldestChange(); // idomain != -1 means that we got one // generalized oldest change to process if (iDom==-1) { closePhase1(); // signify end of phase 1 to the caller return null; } // idomain != -1 means that we got one // generalized oldest change to process String suffix = this.clDomCtxts[iDom].rsd.getBaseDn(); theOldestChange = new ECLUpdateMsg( (LDAPUpdateMsg)clDomCtxts[iDom].nextMsg, null, // set later suffix); nextclchange = false; /* TODO:ECL G Good change log draft compat. if (cLSearchCtxt.nextSeqnum!=-1) { // Should either retrieve or generate a seqnum // we also need to check if the seqnumdb is acccurate reagrding // the changelogdb. // if not, 2 potential reasons // -1 : purge from the changelog .. let's traverse the seqnumdb // -2 : changelog is late .. let's traverse the changelog // replogcn : the oldest change from the changelog db ChangeNumber replogcn = theOldestChange.getChangeNumber(); DN replogReplDomDN = clDomCtxts[iDom].rsd.getBaseDn(); while (true) { if (!cLSearchCtxt.endOfSeqnumdbReached) { // we did not reach yet the end of the seqnumdb // seqnumcn : the next change from from the seqnum db ChangeNumber seqnumcn = seqnumDbReadIterator.getChangeNumber(); // are replogcn and seqnumcn should be the same change ? int cmp = replogcn.compareTo(seqnumcn); DN seqnumReplDomDN=DN.decode(seqnumDbReadIterator. getDomainDN()); TRACER.debugInfo("seqnumgen: comparing the 2 db " + " changelogdb:" + replogReplDomDN + "=" + replogcn + " ts=" + new Date(replogcn.getTime()).toString() + "## seqnumdb:" + seqnumReplDomDN + "=" + seqnumcn + " ts=" + new Date(seqnumcn.getTime()).toString() + " sn older=" + seqnumcn.older(replogcn)); if ((replogReplDomDN.compareTo(seqnumReplDomDN)==0) && (cmp==0)) { // same domain and same CN => same change // assign the seqnum from the seqnumdb // to the change from the changelogdb TRACER.debugInfo("seqnumgen: assigning seqnum=" + seqnumDbReadIterator.getSeqnum() + " to change=" + theOldestChange); theOldestChange.setSeqnum(seqnumDbReadIterator.getSeqnum()); // prepare the next seqnum for the potential next change added // to the seqnumDb cLSearchCtxt.nextSeqnum = seqnumDbReadIterator.getSeqnum() + 1; break; } else { // replogcn and seqnumcn are NOT the same change if (seqnumcn.older(replogcn)) { // the change from the seqnumDb is older // that means that the change has been purged from the // changelog try { // let's traverse the seqnumdb searching for the change // found in the changelogDb. TRACER.debugInfo("seqnumgen: will skip " + seqnumcn + " and next from the seqnum"); cLSearchCtxt.endOfSeqnumdbReached = (seqnumDbReadIterator.next()==false); TRACER.debugInfo("seqnumgen: has nexted cr to " + " sn=" + seqnumDbReadIterator.getSeqnum() + " cn=" + seqnumDbReadIterator.getChangeNumber() + " and reached end " + " of seqnumdb:"+cLSearchCtxt.endOfSeqnumdbReached); if (cLSearchCtxt.endOfSeqnumdbReached) { // we are at the end of the seqnumdb in the append mode // store in seqnumdb the pair // seqnum of the cur change,state before this change) replicationServerDomain.addSeqnum( cLSearchCtxt.nextSeqnum, getGenState(), clDomCtxts[iDom].rsd.getBaseDn().toNormalizedString(), theOldestChange.getChangeNumber()); theOldestChange.setSeqnum(cLSearchCtxt.nextSeqnum); cLSearchCtxt.nextSeqnum++; break; } else { // next change from seqnumdb cLSearchCtxt.nextSeqnum = seqnumDbReadIterator.getSeqnum() + 1; continue; } } catch(Exception e) { } } else { // the change from the changelogDb is older // it should have been stored lately // let's continue to traverse the changelogdb TRACER.debugInfo("seqnumgen: will skip " + replogcn + " and next from the CL"); nextclchange = true; break; // TO BE CHECKED } } } else { // we are at the end of the seqnumdb in the append mode // store in seqnumdb the pair // (seqnum of the current change, state before this change) replicationServerDomain.addSeqnum( cLSearchCtxt.nextSeqnum, getGenState(), clDomCtxts[iDom].rsd.getBaseDn().toNormalizedString(), theOldestChange.getChangeNumber()); theOldestChange.setSeqnum(cLSearchCtxt.nextSeqnum); cLSearchCtxt.nextSeqnum++; break; } } // while seqnum } // nextseqnum !- -1 */ // here we have the right oldest change and in the seqnum case we // have its seqnum // Set and test the domain of the oldestChange see if we reached // the end of the phase for this domain clDomCtxts[iDom].currentState.update( theOldestChange.getUpdateMsg().getChangeNumber()); if (clDomCtxts[iDom].currentState.cover(clDomCtxts[iDom].stopState)) { clDomCtxts[iDom].active = false; } // Test the seqnum of the oldestChange see if we reached // the end of operation /* TODO:ECL G Good changelog draft compat. Not yet implemented if ((cLSearchCtxt.stopSeqnum>0) && (theOldestChange.getSeqnum()>=cLSearchCtxt.stopSeqnum)) { closePhase1(); // means end of phase 1 to the calling writer return null; } */ if (clDomCtxts[iDom].active) { // populates the table with the next eligible msg from idomain // in non blocking mode, return null when no more eligible msg getNextElligibleMessage(iDom); } } // phase ==1 } // while (nextclchange) if (cLSearchCtxt.searchPhase==2) { clDomCtxtsToString("In getGeneralizedNextMessage (persistent): " + "looking for the generalized oldest change"); for (int ido=0; ido<clDomCtxts.length; ido++) { // get next msg getNextElligibleMessage(ido); } // take the oldest one iDom = getGeneralizedOldestChange(); if (iDom != -1) { String suffix = this.clDomCtxts[iDom].rsd.getBaseDn(); theOldestChange = new ECLUpdateMsg( (LDAPUpdateMsg)clDomCtxts[iDom].nextMsg, null, // set later suffix); clDomCtxts[iDom].currentState.update( theOldestChange.getUpdateMsg().getChangeNumber()); /* TODO:ECL G Good changelog draft compat. if (cLSearchCtxt.nextSeqnum!=-1) { // should generate seqnum // store in seqnumdb the pair // (seqnum of the current change, state before this change) replicationServerDomain.addSeqnum( cLSearchCtxt.nextSeqnum, getGenState(), clDomCtxts[iDom].rsd.getBaseDn().toNormalizedString(), theOldestChange.getChangeNumber()); theOldestChange.setSeqnum(cLSearchCtxt.nextSeqnum); cLSearchCtxt.nextSeqnum++; } */ } } } catch(Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); throw new DirectoryException( ResultCode.OPERATIONS_ERROR, Message.raw(Category.SYNC, Severity.INFORMATION,"Exception raised: "), e); } if (theOldestChange != null) { // Update the current state this.cLSearchCtxt.currentCookie.update( theOldestChange.getServiceId(), theOldestChange.getUpdateMsg().getChangeNumber()); // Set the current value of global state in the returned message theOldestChange.setCookie(this.cLSearchCtxt.currentCookie); } return theOldestChange; } /** * Terminates the first (non persistent) phase of the search on the ECL. */ private void closePhase1() { // starvation of changelog messages // all domain have been unactived means are covered if (debugEnabled()) TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + "," + this + " closePhase1()" + " searchCtxt=" + cLSearchCtxt); // go to persistent phase if one for (int i=0; i<clDomCtxts.length; i++) clDomCtxts[i].active = true; if (this.cLSearchCtxt.isPersistent != StartECLSessionMsg.NON_PERSISTENT) { // phase = 1 done AND persistent search => goto phase 2 cLSearchCtxt.searchPhase=2; if (writer ==null) { writer = new ECLServerWriter(session,this,replicationServerDomain); writer.start(); // start suspended } } else { // phase = 1 done AND !persistent search => reinit to phase 0 cLSearchCtxt.searchPhase=0; } /* TODO:ECL G Good changelog draft compat. if (seqnumDbReadIterator!=null) { // End of phase 1 => always release the seqnum iterator seqnumDbReadIterator.releaseCursor(); seqnumDbReadIterator = null; } */ } /** * Get the oldest change contained in the subx table. * The subx table should be populated before * @return the oldest change. */ private int getGeneralizedOldestChange() { int oldest = -1; for (int i=0; i<clDomCtxts.length; i++) { if ((clDomCtxts[i].active)) { // on the first loop, oldest==-1 // .msg is null when the previous (non blocking) nextMessage did // not have any eligible msg to return if (clDomCtxts[i].nextMsg != null) { if ((oldest==-1) || (clDomCtxts[i].nextMsg.compareTo(clDomCtxts[oldest].nextMsg)<0)) { oldest = i; } } } } TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + "," + this + " getGeneralizedOldestChange() " + " returns " + ((oldest!=-1)?clDomCtxts[oldest].nextMsg:"")); return oldest; } /** * Get from the provided domain, the next message elligible regarding * the crossDomain elligible CN. Put it in the context table. * @param idomain the provided domain. */ private void getNextElligibleMessage(int idomain) { ChangeNumber crossDomainElligibleCN = computeCrossDomainElligibleCN(); try { if (clDomCtxts[idomain].nonElligiblemsg != null) { TRACER.debugInfo("getNextElligibleMessage tests if the already " + " stored nonElligibleMsg has becoem elligible regarding " + " the crossDomainElligibleCN ("+crossDomainElligibleCN + " ) " + clDomCtxts[idomain].nonElligiblemsg.getChangeNumber(). older(crossDomainElligibleCN)); // we already got the oldest msg and it was not elligible if (clDomCtxts[idomain].nonElligiblemsg.getChangeNumber(). older(crossDomainElligibleCN)) { // it is now elligible clDomCtxts[idomain].nextMsg = clDomCtxts[idomain].nonElligiblemsg; clDomCtxts[idomain].nonElligiblemsg = null; } else { // the oldest is still not elligible - let's wait next } } else { // non blocking UpdateMsg newMsg = clDomCtxts[idomain].mh.getnextMessage(false); TRACER.debugInfo(this + " getNextElligibleMessage got the next changelogmsg " + " from " + clDomCtxts[idomain].mh.getServiceId() + " newCLMsg=" + newMsg); clDomCtxts[idomain].nextMsg = clDomCtxts[idomain].nonElligiblemsg = null; // in non blocking mode, return null when no more msg if (newMsg != null) { /* TODO:ECL Take into account eligibility. TRACER.debugInfo("getNextElligibleMessage is " + newMsg.getChangeNumber() + new Date(newMsg.getChangeNumber().getTime()).toString() + " elligible " + newMsg.getChangeNumber().older(crossDomainElligibleCN)); if (newMsg.getChangeNumber().older(crossDomainElligibleCN)) { // is elligible clDomCtxts[idomain].nextMsg = newMsg; } else { // is not elligible clDomCtxts[idomain].nonElligiblemsg = newMsg; } */ clDomCtxts[idomain].nextMsg = newMsg; } } } catch(Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } /* */ ECLEligibleCNComputerThread eligibleCNComputerThread = null; ChangeNumber liveecn; private ChangeNumber computeCrossDomainElligibleCN() { return liveecn; } /** * This class specifies the thread that computes periodically * the cross domain eligible CN. */ private final class ECLEligibleCNComputerThread extends DirectoryThread { /** * The tracer object for the debug logger. */ private boolean shutdown = false; private ECLEligibleCNComputerThread() { super("ECL eligible CN computer thread"); start(); } public void run() { while (shutdown == false) { try { synchronized (this) { liveecn = computeNewCrossDomainElligibleCN(); try { this.wait(1000); } catch (InterruptedException e) { } } } catch (Exception end) { break; } } } private ChangeNumber computeNewCrossDomainElligibleCN() { ChangeNumber computedCrossDomainElligibleCN = null; String s = "=> "; ReplicationServer rs = replicationServerDomain.getReplicationServer(); TRACER.debugInfo("ECLSH.computeNewCrossDomainElligibleCN() " + " periodic starts rs="+rs); Iterator<ReplicationServerDomain> rsdi = rs.getCacheIterator(); if (rsdi != null) { while (rsdi.hasNext()) { ReplicationServerDomain domain = rsdi.next(); if (domain.getBaseDn().equalsIgnoreCase("cn=changelog")) continue; ChangeNumber domainElligibleCN = computeEligibleCN(domain); if (domainElligibleCN==null) continue; if ((computedCrossDomainElligibleCN == null) || (domainElligibleCN.older(computedCrossDomainElligibleCN))) { computedCrossDomainElligibleCN = domainElligibleCN; } s += "\n DN:" + domain.getBaseDn() + "\t\t domainElligibleCN :" + domainElligibleCN + "/" + new Date(domainElligibleCN.getTime()).toString(); } } TRACER.debugInfo("SH.computeNewCrossDomainElligibleCN() periodic " + " ends with " + " the following domainElligibleCN for each domain :" + s + "\n thus CrossDomainElligibleCN=" + computedCrossDomainElligibleCN + " ts=" + new Date(computedCrossDomainElligibleCN.getTime()).toString()); return computedCrossDomainElligibleCN; } } /** * Compute the eligible CN. * @param rsd The provided replication server domain for which we want * to retrieve the eligible date. * @return null if the domain does not play in eligibility. */ public ChangeNumber computeEligibleCN(ReplicationServerDomain rsd) { ChangeNumber elligibleCN = null; ServerState heartbeatState = rsd.getHeartbeatState(); if (heartbeatState==null) return null; // compute elligible CN ServerState hbState = heartbeatState.duplicate(); Iterator<Short> it = hbState.iterator(); while (it.hasNext()) { short sid = it.next(); ChangeNumber storedCN = hbState.getMaxChangeNumber(sid); // If the most recent UpdateMsg or CLHeartbeatMsg received is very old // then the server is considered down and not considered for eligibility if (TimeThread.getTime()-storedCN.getTime()>2000) { TRACER.debugInfo( "For RSD." + rsd.getBaseDn() + " Server " + sid + " is not considered for eligibility ... potentially down"); continue; } if ((elligibleCN == null) || (storedCN.older(elligibleCN))) { elligibleCN = storedCN; } } TRACER.debugInfo( "For RSD." + rsd.getBaseDn() + " ElligibleCN()=" + elligibleCN); return elligibleCN; } /** * Returns the client operation id. * @return The client operation id. */ public String getOperationId() { return operationId; } /** * Getter for the persistent property of the current search. * @return Whether the current search is persistent or not. */ public short isPersistent() { return this.cLSearchCtxt.isPersistent; } /** * Getter for the current search phase (INIT or PERSISTENT). * @return Whether the current search is persistent or not. */ public int getSearchPhase() { return this.cLSearchCtxt.searchPhase; } } opends/src/server/org/opends/server/replication/server/ECLServerWriter.java
New file @@ -0,0 +1,306 @@ /* * 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 2009 Sun Microsystems, Inc. */ 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.StaticUtils.stackTraceToSingleLineString; import java.io.IOException; import java.net.SocketException; import org.opends.messages.Message; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PersistentSearch; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.protocol.DoneMsg; import org.opends.server.replication.protocol.ECLUpdateMsg; import org.opends.server.replication.protocol.ProtocolSession; import org.opends.server.replication.protocol.StartECLSessionMsg; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.workflowelement.externalchangelog.ECLSearchOperation; import org.opends.server.workflowelement.externalchangelog.ECLWorkflowElement; /** * This class defines a server writer, which is used to send changes to a * directory server. */ public class ECLServerWriter extends ServerWriter { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); private ProtocolSession session; private ECLServerHandler handler; private ReplicationServerDomain replicationServerDomain; private short protocolVersion = -1; private boolean suspended; private boolean shutdown; /** * Create a ServerWriter. * * @param session the ProtocolSession that will be used to send updates. * @param handler ECL handler for which the ServerWriter is created. * @param replicationServerDomain the ReplicationServerDomain of this * ServerWriter. */ public ECLServerWriter(ProtocolSession session, ECLServerHandler handler, ReplicationServerDomain replicationServerDomain) { super(session, (short)-1, handler, replicationServerDomain); setName("Replication ECL Writer Thread for op:" + handler.getOperationId()); this.session = session; this.handler = handler; this.replicationServerDomain = replicationServerDomain; // Keep protocol version locally for efficiency this.protocolVersion = handler.getProtocolVersion(); this.suspended = false; this.shutdown = false; } /** * The writer will start suspended by the Handler for the CL * waiting for the startCLSessionMsg. Then it may be * suspended between 2 jobs, each job being a separate search. */ public synchronized void suspendWriter() { synchronized(this) { suspended = true; } } /** * Resume the writer. */ public synchronized void resumeWriter() { synchronized(this) { suspended = false; } notify(); } /** * Run method for the ServerWriter. * Loops waiting for changes from the ReplicationServerDomain and * forward them to the other servers */ public void run() { Message errMessage = null; try { while (true) { // wait to be resumed or shutdowned if ((suspended) && (!shutdown)) { synchronized(this) { wait(); } } if (shutdown) { return; } // Not suspended doIt(); if (shutdown) { return; } suspendWriter(); } } catch (SocketException e) { // Just ignore the exception and let the thread die as well errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(), "for operation " + handler.getOperationId()); logError(errMessage); } catch (Exception e) { // An unexpected error happened. // Log an error and close the connection. errMessage = ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() + " " + stackTraceToSingleLineString(e)); logError(errMessage); } finally { if (session!=null) { try { session.close(); } catch (IOException e) { // Can't do much more : ignore } } replicationServerDomain.stopServer(handler); } } /** * Loop gettting changes from the domain and publishing them either to * the provided session or to the ECL session interface. * @throws IOException when raised (connection closure) * @throws InterruptedException when raised */ public void doIt() throws IOException, InterruptedException { ECLUpdateMsg update = null; while (true) { if (shutdown) { return; } if (suspended) { return; } // TODO:ECL please document the details of the cases where update could // be not null here if (update == null) { try { update = handler.takeECLUpdate(); } catch(DirectoryException de) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } } if (update == null) { if (handler.getSearchPhase() != 1) { if (session!=null) { // session is null in pusherOnly mode // Done is used to end phase 1 session.publish(new DoneMsg( replicationServerDomain.getReplicationServer().getServerId(), handler.getServerId()), protocolVersion); } } if (handler.isPersistent() == StartECLSessionMsg.NON_PERSISTENT) { // publishing is normally stopped here break; } else { // except if we are in persistent search Thread.sleep(200); continue; } } else { // Publish the update to the remote server using a protocol version he // it supports publish(update, protocolVersion); update = null; } } } /** * Shutdown the writer. */ public synchronized void shutdownWriter() { shutdown = true; this.notify(); } /** * Publish a change either on the protocol session or to a persistent search. */ private void publish(ECLUpdateMsg msg, short reqProtocolVersion) throws IOException { if (debugEnabled()) TRACER.debugInfo(this + " publishes msg=[" + msg.toString() + "]"); if (session!=null) { session.publish(msg, protocolVersion); } else { ECLWorkflowElement wfe = (ECLWorkflowElement) DirectoryServer.getWorkflowElement( ECLWorkflowElement.ECL_WORKFLOW_ELEMENT); // Notify persistent searches. for (PersistentSearch psearch : wfe.getPersistentSearches()) { try { Entry eclEntry = ECLSearchOperation.createEntryFromMsg(msg); psearch.processAdd(eclEntry, -1); } catch(Exception e) { Message errMessage = ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() + " " + stackTraceToSingleLineString(e)); logError(errMessage); } } } } } opends/src/server/org/opends/server/replication/server/ExternalChangeLogSessionImpl.java
New file @@ -0,0 +1,85 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.replication.server; import org.opends.server.replication.common.ExternalChangeLogSession; import org.opends.server.replication.protocol.ECLUpdateMsg; import org.opends.server.replication.protocol.StartECLSessionMsg; import org.opends.server.types.DirectoryException; /** * This interface defines a session used to search the external changelog * in the Directory Server. */ public class ExternalChangeLogSessionImpl implements ExternalChangeLogSession { ECLServerHandler handler; ReplicationServer rs; /** * Create a new external changelog session. * @param rs The replication server to which we will request the log. * @param startECLSessionMsg The start session message containing the * details of the search request on the ECL. * @throws DirectoryException When an error occurs. */ public ExternalChangeLogSessionImpl( ReplicationServer rs, StartECLSessionMsg startECLSessionMsg) throws DirectoryException { this.rs = rs; this.handler = new ECLServerHandler( rs.getServerURL(), rs.getServerId(), rs, startECLSessionMsg); } /** * Returns the next message available for the ECL (blocking) * null when none. * @return the next available message from the ECL. * @throws DirectoryException when needed. */ public ECLUpdateMsg getNextUpdate() throws DirectoryException { return handler.getnextUpdate(); } /** * Close the session. */ public void close() { handler.getDomain().stopServer(handler); } } opends/src/server/org/opends/server/replication/server/LightweightServerHandler.java
@@ -64,7 +64,7 @@ // The tracer object for the debug logger. private static final DebugTracer TRACER = getTracer(); private ServerHandler replServerHandler; private ReplicationServerHandler replServerHandler; private ReplicationServerDomain rsDomain; // The id of the RS this DS is connected to private short replicationServerId = -1; @@ -103,7 +103,7 @@ * @param assuredMode The assured mode of the remote DS * @param safeDataLevel The safe data level of the remote DS */ public LightweightServerHandler(ServerHandler replServerHandler, public LightweightServerHandler(ReplicationServerHandler replServerHandler, short replicationServerId, short serverId, long generationId, byte groupId, ServerStatus status, List<String> refUrls, boolean assuredFlag, AssuredMode assuredMode, byte safeDataLevel) opends/src/server/org/opends/server/replication/server/MessageHandler.java
New file @@ -0,0 +1,912 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.replication.server; import static org.opends.messages.ReplicationMessages.ERR_RS_DN_DOES_NOT_MATCH; import static org.opends.server.loggers.debug.DebugLogger.*; import java.util.ArrayList; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.atomic.AtomicBoolean; import org.opends.messages.Message; import org.opends.server.admin.std.server.MonitorProviderCfg; import org.opends.server.api.MonitorProvider; import org.opends.server.config.ConfigException; import org.opends.server.core.DirectoryServer; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.ServerState; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.types.Attribute; import org.opends.server.types.Attributes; import org.opends.server.types.DirectoryException; import org.opends.server.types.InitializationException; import org.opends.server.types.ResultCode; import org.opends.server.util.TimeThread; /** * This class implements a buffering/producer/consumer mechanism of * replication changes (UpdateMsg) used inside the replication server. * * MessageHandlers are registered into Replication server domains. * When an update message is received by a domain, the domain forwards * the message to the registered message handlers. * Message are buffered into a queue. * Consumers are expected to come and consume the UpdateMsg from the queue. */ public class MessageHandler extends MonitorProvider<MonitorProviderCfg> { /** * The tracer object for the debug logger. */ protected static final DebugTracer TRACER = getTracer(); /** * UpdateMsg queue. */ private final MsgQueue msgQueue = new MsgQueue(); /** * Late queue. */ protected MsgQueue lateQueue = new MsgQueue(); /** * Local hosting RS. */ protected ReplicationServer replicationServer = null; /** * The URL of the hosting replication server. */ protected String replicationServerURL = null; /** * The serverID of the hosting replication server. */ protected short replicationServerId; /** * Specifies the related replication server domain based on serviceId(baseDn). */ protected ReplicationServerDomain replicationServerDomain = null; /** * Number of update sent to the server. */ protected int outCount = 0; /** * Number of updates received from the server. */ protected int inCount = 0; /** * Specifies the max receive queue for this handler. */ protected int maxReceiveQueue = 0; /** * Specifies the max send queue for this handler. */ protected int maxSendQueue = 0; /** * Specifies the max receive delay for this handler. */ protected int maxReceiveDelay = 0; /** * Specifies the max send delay for this handler. */ protected int maxSendDelay = 0; /** * Specifies the max queue size for this handler. */ protected int maxQueueSize = 5000; /** * Specifies the max queue size in bytes for this handler. */ protected int maxQueueBytesSize = maxQueueSize * 100; /** * Specifies the max restart receive queue for this handler. */ protected int restartReceiveQueue; /** * Specifies the max restart send queue for this handler. */ protected int restartSendQueue; /** * Specifies the max restart receive delay for this handler. */ protected int restartReceiveDelay; /** * Specifies the max restart send delay for this handler. */ protected int restartSendDelay; /** * Specifies whether the consumer is following the producer (is not late). */ protected boolean following = false; /** * Specifies the current serverState of this handler. */ private ServerState serverState; /** * Specifies the identifier of the service (usually the baseDn of the domain). */ private String serviceId = null; /** * Specifies whether the server is flow controlled and should be stopped from * sending messages. */ protected boolean flowControl = false; /** * Specifies whether the consumer is still active or not. * If not active, the handler will not return any message. * Called at the beginning of shutdown process. */ private boolean activeConsumer = true; /** * Set when ServerHandler is stopping. */ private AtomicBoolean shuttingDown = new AtomicBoolean(false); /** * Creates a new server handler instance with the provided socket. * @param queueSize The maximum number of update that will be kept * in memory by this ServerHandler. * @param replicationServerURL The URL of the hosting replication server. * @param replicationServerId The ID of the hosting replication server. * @param replicationServer The hosting replication server. */ public MessageHandler( int queueSize, String replicationServerURL, short replicationServerId, ReplicationServer replicationServer) { super("Message Handler"); this.maxQueueSize = queueSize; this.maxQueueBytesSize = queueSize * 100; this.replicationServerURL = replicationServerURL; this.replicationServerId = replicationServerId; this.replicationServer = replicationServer; } /** * Add an update to the list of updates that must be sent to the server * managed by this Handler. * * @param update The update that must be added to the list of updates of * this handler. * @param sourceHandler The source handler that generated the update. */ public void add(UpdateMsg update, MessageHandler sourceHandler) { synchronized (msgQueue) { /* * If queue was empty the writer thread was probably asleep * waiting for some changes, wake it up */ if (msgQueue.isEmpty()) msgQueue.notify(); msgQueue.add(update); /* TODO : size should be configurable * and larger than max-receive-queue-size */ while ((msgQueue.count() > maxQueueSize) || (msgQueue.bytesCount() > maxQueueBytesSize)) { setFollowing(false); msgQueue.removeFirst(); } } if (isSaturated(update.getChangeNumber(), sourceHandler)) { sourceHandler.setSaturated(true); } } /** * Set the shut down flag to true and returns the previous value of the flag. * @return The previous value of the shut down flag */ public boolean engageShutdown() { // Use thread safe boolean return shuttingDown.getAndSet(true); } /** * Get an approximation of the delay by looking at the age of the oldest * message that has not been sent to this server. * This is an approximation because the age is calculated using the * clock of the server where the replicationServer is currently running * while it should be calculated using the clock of the server * that originally processed the change. * * The approximation error is therefore the time difference between * * @return the approximate delay for the connected server. */ public long getApproxDelay() { long olderUpdateTime = getOlderUpdateTime(); if (olderUpdateTime == 0) return 0; long currentTime = TimeThread.getTime(); return ((currentTime - olderUpdateTime) / 1000); } /** * Get the age of the older change that has not yet been replicated * to the server handled by this ServerHandler. * @return The age if the older change has not yet been replicated * to the server handled by this ServerHandler. */ public Long getApproxFirstMissingDate() { Long result = (long) 0; // Get the older CN received ChangeNumber olderUpdateCN = getOlderUpdateCN(); if (olderUpdateCN != null) { // If not present in the local RS db, // then approximate with the older update time result = olderUpdateCN.getTime(); } return result; } /** * Returns the Replication Server Domain to which belongs this handler. * @param createIfNotExist Creates the domain if it does not exist. * @return The replication server domain. */ public ReplicationServerDomain getDomain(boolean createIfNotExist) { if (replicationServerDomain==null) { replicationServerDomain = replicationServer.getReplicationServerDomain(serviceId,createIfNotExist); } return replicationServerDomain; } /** * Get the count of updates received from the server. * @return the count of update received from the server. */ public int getInCount() { return inCount; } /** * 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() { ArrayList<Attribute> attributes = new ArrayList<Attribute>(); attributes.add(Attributes.create("handler", getMonitorInstanceName())); attributes.add( Attributes.create("queue-size", String.valueOf(msgQueue.count()))); attributes.add( Attributes.create( "queue-size-bytes", String.valueOf(msgQueue.bytesCount()))); attributes.add( Attributes.create( "following", String.valueOf(following))); return attributes; } /** * Retrieves the name of this monitor provider. It should be unique among all * monitor providers, including all instances of the same monitor provider. * * @return The name of this monitor provider. */ @Override public String getMonitorInstanceName() { return "Message Handler"; } /** * Get the next update that must be sent to the consumer * from the message queue or from the database. * * @param synchronous specifies what to do when the queue is empty. * when true, the method blocks; when false the method return null. * @return The next update that must be sent to the consumer. * null when synchronous is false and queue is empty. */ protected UpdateMsg getnextMessage(boolean synchronous) { UpdateMsg msg; while (activeConsumer == true) { if (following == false) { /* this server is late with regard to some other masters * in the topology or just joined the topology. * In such cases, we can't keep all changes in the queue * without saturating the memory, we therefore use * a lateQueue that is filled with a few changes from the changelogDB * If this server is able to close the gap, it will start using again * the regular msgQueue later. */ if (lateQueue.isEmpty()) { /* * Start from the server State * Loop until the queue high mark or until no more changes * for each known LDAP master * get the next CSN after this last one : * - try to get next from the file * - if not found in the file * - try to get the next from the queue * select the smallest of changes * check if it is in the memory tree * yes : lock memory tree. * check all changes from the list, remove the ones that * are already sent * unlock memory tree * restart as usual * load this change on the delayList * */ ReplicationIteratorComparator comparator = new ReplicationIteratorComparator(); SortedSet<ReplicationIterator> iteratorSortedSet = new TreeSet<ReplicationIterator>(comparator); /* fill the lateQueue */ for (short serverId : replicationServerDomain.getServers()) { ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId); ReplicationIterator iterator = replicationServerDomain.getChangelogIterator(serverId, lastCsn); if (iterator != null) { if (iterator.getChange() != null) { iteratorSortedSet.add(iterator); } else { iterator.releaseCursor(); } } } // The loop below relies on the fact that it is sorted based // on the currentChange of each iterator to consider the next // change across all servers. // Hence it is necessary to remove and eventual add again an iterator // when looping in order to keep consistent the order of the // iterators (see ReplicationIteratorComparator. while (!iteratorSortedSet.isEmpty() && (lateQueue.count()<100) && (lateQueue.bytesCount()<50000) ) { ReplicationIterator iterator = iteratorSortedSet.first(); iteratorSortedSet.remove(iterator); lateQueue.add(iterator.getChange()); if (iterator.next()) iteratorSortedSet.add(iterator); else iterator.releaseCursor(); } for (ReplicationIterator iterator : iteratorSortedSet) { iterator.releaseCursor(); } /* * Check if the first change in the lateQueue is also on the regular * queue */ if (lateQueue.isEmpty()) { synchronized (msgQueue) { if ((msgQueue.count() < maxQueueSize) && (msgQueue.bytesCount() < maxQueueBytesSize)) { setFollowing(true); } } } else { msg = lateQueue.first(); synchronized (msgQueue) { if (msgQueue.contains(msg)) { /* we finally catch up with the regular queue */ setFollowing(true); lateQueue.clear(); UpdateMsg msg1; do { msg1 = msgQueue.removeFirst(); } while (!msg.getChangeNumber().equals(msg1.getChangeNumber())); this.updateServerState(msg); return msg; } } } } else { /* get the next change from the lateQueue */ msg = lateQueue.removeFirst(); this.updateServerState(msg); return msg; } } synchronized (msgQueue) { if (following == true) { try { while (msgQueue.isEmpty() && (following == true)) { if (!synchronous) return null; msgQueue.wait(500); if (!activeConsumer) return null; } } catch (InterruptedException e) { return null; } msg = msgQueue.removeFirst(); if (this.updateServerState(msg)) { /* * Only push the message if it has not yet been seen * by the other server. * Otherwise just loop to select the next message. */ return msg; } } } /* * Need to loop because following flag may have gone to false between * the first check at the beginning of this method * and the second check just above. */ } return null; } /** * Get the older Change Number for that server. * Returns null when the queue is empty. * @return The older change number. */ public ChangeNumber getOlderUpdateCN() { ChangeNumber result = null; synchronized (msgQueue) { if (isFollowing()) { if (msgQueue.isEmpty()) { result = null; } else { UpdateMsg msg = msgQueue.first(); result = msg.getChangeNumber(); } } else { if (lateQueue.isEmpty()) { // isFollowing is false AND lateQueue is empty // We may be at the very moment when the writer has emptyed the // lateQueue when it sent the last update. The writer will fill again // the lateQueue when it will send the next update but we are not yet // there. So let's take the last change not sent directly from // the db. ReplicationIteratorComparator comparator = new ReplicationIteratorComparator(); SortedSet<ReplicationIterator> iteratorSortedSet = new TreeSet<ReplicationIterator>(comparator); try { // Build a list of candidates iterator (i.e. db i.e. server) for (short serverId : replicationServerDomain.getServers()) { // get the last already sent CN from that server ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId); // get an iterator in this server db from that last change ReplicationIterator iterator = replicationServerDomain.getChangelogIterator(serverId, lastCsn); // if that iterator has changes, then it is a candidate // it is added in the sorted list at a position given by its // current change (see ReplicationIteratorComparator). if ((iterator != null) && (iterator.getChange() != null)) { iteratorSortedSet.add(iterator); } } UpdateMsg msg = iteratorSortedSet.first().getChange(); result = msg.getChangeNumber(); } catch (Exception e) { result = null; } finally { for (ReplicationIterator iterator : iteratorSortedSet) { iterator.releaseCursor(); } } } else { UpdateMsg msg = lateQueue.first(); result = msg.getChangeNumber(); } } } return result; } /** * Get the older update time for that server. * @return The older update time. */ public long getOlderUpdateTime() { ChangeNumber olderUpdateCN = getOlderUpdateCN(); if (olderUpdateCN == null) return 0; return olderUpdateCN.getTime(); } /** * Get the count of updates sent to this server. * @return The count of update sent to this server. */ public int getOutCount() { return outCount; } /** * Get the number of message in the receive message queue. * @return Size of the receive message queue. */ public int getRcvMsgQueueSize() { synchronized (msgQueue) { /* * When the server is up to date or close to be up to date, * the number of updates to be sent is the size of the receive queue. */ if (isFollowing()) return msgQueue.count(); else { /** * When the server is not able to follow, the msgQueue * may become too large and therefore won't contain all the * changes. Some changes may only be stored in the backing DB * of the servers. * The total size of the receive queue is calculated by doing * the sum of the number of missing changes for every dbHandler. */ ServerState dbState = replicationServerDomain.getDbServerState(); return ServerState.diffChanges(dbState, serverState); } } } /** * Get the state of this server. * * @return ServerState the state for this server.. */ public ServerState getServerState() { return serverState; } /** * Get the name of the serviceId (usually baseDn) for this handler. * @return The name of the serviceId. */ protected String getServiceId() { return serviceId; } /** * Retrieves the length of time in milliseconds that should elapse between * calls to the <CODE>updateMonitorData()</CODE> method. A negative or zero * return value indicates that the <CODE>updateMonitorData()</CODE> method * should not be periodically invoked. * * @return The length of time in milliseconds that should elapse between * calls to the <CODE>updateMonitorData()</CODE> method. */ @Override public long getUpdateInterval() { /* we don't wont to do polling on this monitor */ return 0; } /** * Increase the counter of update received from the server. */ public void incrementInCount() { inCount++; } /** * Increase the counter of updates sent to the server. */ public void incrementOutCount() { outCount++; } /** * {@inheritDoc} */ @Override public void initializeMonitorProvider(MonitorProviderCfg configuration) throws ConfigException, InitializationException { // Nothing to do for now } /** * Check if the LDAP server can follow the speed of the other servers. * @return true when the server has all the not yet sent changes * in its queue. */ public boolean isFollowing() { return following; } /** * Check is this server is saturated (this server has already been * sent a bunch of updates and has not processed them so they are staying * in the message queue for this server an the size of the queue * for this server is above the configured limit. * * The limit can be defined in number of updates or with a maximum delay * * @param changeNumber The changenumber to use to make the delay calculations. * @param sourceHandler The ServerHandler which is sending the update. * @return true is saturated false if not saturated. */ public boolean isSaturated(ChangeNumber changeNumber, MessageHandler sourceHandler) { synchronized (msgQueue) { int size = msgQueue.count(); if ((maxReceiveQueue > 0) && (size >= maxReceiveQueue)) return true; if ((sourceHandler.maxSendQueue > 0) && (size >= sourceHandler.maxSendQueue)) return true; if (!msgQueue.isEmpty()) { UpdateMsg firstUpdate = msgQueue.first(); if (firstUpdate != null) { long timeDiff = changeNumber.getTimeSec() - firstUpdate.getChangeNumber().getTimeSec(); if ((maxReceiveDelay > 0) && (timeDiff >= maxReceiveDelay)) return true; if ((sourceHandler.maxSendDelay > 0) && (timeDiff >= sourceHandler.maxSendDelay)) return true; } } return false; } } /** * Check that the size of the Server Handler messages Queue has lowered * below the limit and therefore allowing the reception of messages * from other servers to restart. * @param source The ServerHandler which was sending the update. * can be null. * @return true if the processing can restart */ public boolean restartAfterSaturation(MessageHandler source) { synchronized (msgQueue) { int queueSize = msgQueue.count(); if ((maxReceiveQueue > 0) && (queueSize >= restartReceiveQueue)) return false; if ((source != null) && (source.maxSendQueue > 0) && (queueSize >= source.restartSendQueue)) return false; if (!msgQueue.isEmpty()) { UpdateMsg firstUpdate = msgQueue.first(); UpdateMsg lastUpdate = msgQueue.last(); if ((firstUpdate != null) && (lastUpdate != null)) { long timeDiff = lastUpdate.getChangeNumber().getTimeSec() - firstUpdate.getChangeNumber().getTimeSec(); if ((maxReceiveDelay > 0) && (timeDiff >= restartReceiveDelay)) return false; if ((source != null) && (source.maxSendDelay > 0) && (timeDiff >= source.restartSendDelay)) return false; } } } return true; } /** * Set that the consumer is now becoming inactive and thus getNextMessage * should not return any UpdateMsg any more. * @param active the provided state of the consumer. */ public void setConsumerActive(boolean active) { this.activeConsumer = active; } /** * Set the following flag of this server. * @param following the value that should be set. */ private void setFollowing(boolean following) { this.following = following; } private void setSaturated(boolean value) { flowControl = value; } /** * Set the initial value of the serverState for this handler. * Expected to be done once, then the state will be updated using * updateServerState() method. * @param serverState the provided serverState. * @exception DirectoryException raised when a problem occurs. */ public void setInitialServerState(ServerState serverState) throws DirectoryException { this.serverState = serverState; } /** * Set the serviceId (usually baseDn) for this handler. Expected to be done * once and never changed during the handler life. * @param serviceId The provided serviceId. * @exception DirectoryException raised when a problem occurs. */ protected void setServiceIdAndDomain(String serviceId) throws DirectoryException { if (this.serviceId != null) { if (!this.serviceId.equalsIgnoreCase(serviceId)) { Message message = ERR_RS_DN_DOES_NOT_MATCH.get( this.serviceId.toString(), serviceId.toString()); throw new DirectoryException(ResultCode.OTHER, message, null); } } else { this.serviceId = serviceId; this.replicationServerDomain = getDomain(true); } } /** * Shutdown this handler. */ public void shutdown() { /* * Shutdown ServerWriter */ synchronized (msgQueue) { msgQueue.clear(); msgQueue.notify(); msgQueue.notifyAll(); } DirectoryServer.deregisterMonitorProvider(getMonitorInstanceName()); } /** * Performs any processing periodic processing that may be desired to update * the information associated with this monitor. Note that best-effort * attempts will be made to ensure that calls to this method come * <CODE>getUpdateInterval()</CODE> milliseconds apart, but no guarantees will * be made. */ @Override public void updateMonitorData() { // As long as getUpdateInterval() returns 0, this will never get called } /** * Update the serverState with the last message sent. * * @param msg the last update sent. * @return boolean indicating if the update was meaningful. */ public boolean updateServerState(UpdateMsg msg) { return serverState.update(msg.getChangeNumber()); } /** * Get the groupId of the hosting RS. * @return the group id. */ public byte getLocalGroupId() { return replicationServer.getGroupId(); } } opends/src/server/org/opends/server/replication/server/ReplicationServer.java
@@ -42,16 +42,18 @@ import java.net.UnknownHostException; import java.util.ArrayList; import java.util.Collection; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; 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.server.ReplicationServerCfg; import org.opends.server.api.Backend; @@ -61,13 +63,22 @@ import org.opends.server.api.RestoreTaskListener; 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.LogLevel; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.common.ExternalChangeLogSession; import org.opends.server.replication.protocol.ProtocolSession; import org.opends.server.replication.protocol.ReplServerStartMsg; import org.opends.server.replication.protocol.ReplSessionSecurity; import org.opends.server.replication.protocol.ReplicationMsg; import org.opends.server.replication.protocol.ServerStartECLMsg; import org.opends.server.replication.protocol.ServerStartMsg; import org.opends.server.replication.protocol.StartECLSessionMsg; import org.opends.server.types.BackupConfig; import org.opends.server.types.ConfigChangeResult; import org.opends.server.types.DN; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.LDIFExportConfig; @@ -75,7 +86,9 @@ import org.opends.server.types.RestoreConfig; import org.opends.server.types.ResultCode; 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; @@ -155,6 +168,8 @@ */ private static final DebugTracer TRACER = getTracer(); private String externalChangeLogWorkflowID = "External Changelog Workflow ID"; ECLWorkflowElement eclwe; private static HashSet<Integer> localPorts = new HashSet<Integer>(); /** @@ -267,9 +282,35 @@ ReplSessionSecurity.HANDSHAKE_TIMEOUT); if (session == null) // Error, go back to accept continue; ServerHandler handler = new ServerHandler(session, queueSize); handler.start(null, serverId, serverURL, rcvWindow, false, this); 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) { @@ -277,6 +318,10 @@ // 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 == false) { Message message = ERR_EXCEPTION_LISTENING.get(e.getLocalizedMessage()); @@ -379,20 +424,20 @@ /** * Establish a connection to the server with the address and port. * * @param serverURL The address and port for the server, separated by a * @param remoteServerURL The address and port for the server, separated by a * colon. * @param baseDn The baseDn of the connection */ private void connect(String serverURL, String baseDn) private void connect(String remoteServerURL, String baseDn) { int separator = serverURL.lastIndexOf(':'); String port = serverURL.substring(separator + 1); String hostname = serverURL.substring(0, separator); boolean sslEncryption = replSessionSecurity.isSslEncryption(serverURL); 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 " + serverURL); " connects to " + remoteServerURL); try { @@ -402,12 +447,25 @@ socket.setTcpNoDelay(true); socket.connect(ServerAddr, 500); /* ServerHandler handler = new ServerHandler( replSessionSecurity.createClientSession(serverURL, socket, ReplSessionSecurity.HANDSHAKE_TIMEOUT), queueSize); handler.start(baseDn, serverId, this.serverURL, rcvWindow, sslEncryption, this); */ ReplicationServerHandler handler = new ReplicationServerHandler( replSessionSecurity.createClientSession(remoteServerURL, socket, ReplSessionSecurity.HANDSHAKE_TIMEOUT), queueSize, this.serverURL, serverId, this, rcvWindow); handler.connect(baseDn, sslEncryption); } catch (Exception e) { @@ -470,6 +528,10 @@ serverId , this); listenThread.start(); // Initialize the External Changelog // FIXME: how is WF creation enabed/disabled in the RS ? initializeECL(); if (debugEnabled()) TRACER.debugInfo("RS " +getMonitorInstanceName()+ " successfully initialized"); @@ -493,10 +555,88 @@ Message message = ERR_COULD_NOT_BIND_CHANGELOG.get(changelogPort, 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); } } /** * Initializes the ECL access by creating a dedicated workflow element. * @throws DirectoryException */ private void initializeECL() throws DirectoryException { WorkflowImpl externalChangeLogWorkflow; if (WorkflowImpl.getWorkflow(externalChangeLogWorkflowID) !=null) return; ECLWorkflowElement eclwe = new ECLWorkflowElement(this); // Create the workflow for the base DN and register the workflow with // the server. externalChangeLogWorkflow = new WorkflowImpl( externalChangeLogWorkflowID, DN.decode(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT), eclwe.getWorkflowElementID(), eclwe); externalChangeLogWorkflow.register(); NetworkGroup defaultNetworkGroup = NetworkGroup.getDefaultNetworkGroup(); defaultNetworkGroup.registerWorkflow(externalChangeLogWorkflow); // FIXME:ECL should the ECL Workflow be registered in adminNetworkGroup? NetworkGroup adminNetworkGroup = NetworkGroup.getAdminNetworkGroup(); adminNetworkGroup.registerWorkflow(externalChangeLogWorkflow); // FIXME:ECL should the ECL Workflow be registered in internalNetworkGroup? NetworkGroup internalNetworkGroup = NetworkGroup.getInternalNetworkGroup(); internalNetworkGroup.registerWorkflow(externalChangeLogWorkflow); } private void finalizeECL() { 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(); } } /** * Get the ReplicationServerDomain associated to the base DN given in * parameter. * @@ -571,6 +711,8 @@ { dbEnv.shutdown(); } finalizeECL(); } @@ -1220,6 +1362,38 @@ } /** * Returns the number of domains managed by this replication server. * @return the number of domains managed. */ public int getCacheSize() { return baseDNs.size(); } /** * 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 { ExternalChangeLogSessionImpl session = new ExternalChangeLogSessionImpl(this, msg); return session; } /** * Getter on the server URL. * @return the server URL. */ public String getServerURL() { return this.serverURL; } /** * This method allows to check if the Replication Server given * as the parameter is running in the local JVM. * opends/src/server/org/opends/server/replication/server/ReplicationServerDomain.java
@@ -26,56 +26,59 @@ */ package org.opends.server.replication.server; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import static org.opends.server.loggers.debug.DebugLogger.*; import org.opends.server.admin.std.server.MonitorProviderCfg; import org.opends.server.api.MonitorProvider; import org.opends.server.core.DirectoryServer; import org.opends.server.loggers.debug.DebugTracer; import static org.opends.server.loggers.ErrorLogger.logError; 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.StaticUtils.stackTraceToSingleLineString; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.TimeUnit; import java.util.Iterator; import java.util.concurrent.locks.ReentrantLock; 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.std.server.MonitorProviderCfg; import org.opends.server.api.MonitorProvider; import org.opends.server.core.DirectoryServer; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.common.AssuredMode; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.DSInfo; import org.opends.server.replication.common.RSInfo; import org.opends.server.replication.common.ServerState; import org.opends.server.replication.common.ServerStatus; import org.opends.server.replication.common.StatusMachineEvent; import org.opends.server.replication.protocol.AckMsg; import org.opends.server.replication.protocol.ChangeStatusMsg; import org.opends.server.replication.protocol.ErrorMsg; import org.opends.server.replication.protocol.RoutableMsg; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.protocol.TopologyMsg; import org.opends.server.replication.protocol.MonitorMsg; import org.opends.server.replication.protocol.MonitorRequestMsg; import org.opends.server.replication.protocol.ProtocolVersion; import org.opends.server.replication.protocol.ResetGenerationIdMsg; import org.opends.server.replication.protocol.RoutableMsg; import org.opends.server.replication.protocol.TopologyMsg; import org.opends.server.replication.protocol.UpdateMsg; 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; import org.opends.server.util.TimeThread; import com.sleepycat.je.DatabaseException; import java.util.Timer; import java.util.TimerTask; import java.util.concurrent.locks.ReentrantLock; import org.opends.server.replication.common.AssuredMode; import org.opends.server.replication.common.DSInfo; import org.opends.server.replication.common.RSInfo; import org.opends.server.replication.common.ServerStatus; import org.opends.server.replication.common.StatusMachineEvent; import org.opends.server.replication.protocol.AckMsg; import org.opends.server.replication.protocol.ChangeStatusMsg; import org.opends.server.replication.protocol.ProtocolVersion; /** * This class define an in-memory cache that will be used to store @@ -111,8 +114,8 @@ * to this replication server. * */ private final Map<Short, ServerHandler> directoryServers = new ConcurrentHashMap<Short, ServerHandler>(); private final Map<Short, DataServerHandler> directoryServers = new ConcurrentHashMap<Short, DataServerHandler>(); /* * This map contains one ServerHandler for each replication servers @@ -123,8 +126,11 @@ * We add new TreeSet in the HashMap when a new replication server register * to this replication server. */ private final Map<Short, ServerHandler> replicationServers = new ConcurrentHashMap<Short, ServerHandler>(); private final Map<Short, ReplicationServerHandler> replicationServers = new ConcurrentHashMap<Short, ReplicationServerHandler>(); private final ConcurrentLinkedQueue<MessageHandler> otherHandlers = new ConcurrentLinkedQueue<MessageHandler>(); /* * This map contains the List of updates received from each @@ -134,16 +140,14 @@ new ConcurrentHashMap<Short, DbHandler>(); private ReplicationServer replicationServer; /* GenerationId management */ // GenerationId management private long generationId = -1; private boolean generationIdSavedStatus = false; /** * The tracer object for the debug logger. */ // The tracer object for the debug logger. private static final DebugTracer TRACER = getTracer(); /* Monitor data management */ // Monitor data management /** * The monitor data consolidated over the topology. */ @@ -346,9 +350,9 @@ /* * Push the message to the replication servers */ if (sourceHandler.isLDAPserver()) if (sourceHandler.isDataServer()) { for (ServerHandler handler : replicationServers.values()) for (ReplicationServerHandler handler : replicationServers.values()) { /** * Ignore updates to RS with bad gen id @@ -397,7 +401,7 @@ /* * Push the message to the LDAP servers */ for (ServerHandler handler : directoryServers.values()) for (DataServerHandler handler : directoryServers.values()) { // Don't forward the change to the server that just sent it if (handler == sourceHandler) @@ -467,6 +471,14 @@ handler.add(update, sourceHandler); } } // Push the message to the other subscribing handlers Iterator<MessageHandler> otherIter = otherHandlers.iterator(); while (otherIter.hasNext()) { MessageHandler handler = otherIter.next(); handler.add(update, sourceHandler); } } /** @@ -522,10 +534,10 @@ if (sourceGroupId == groupId) // Assured feature does not cross different group ids { if (sourceHandler.isLDAPserver()) if (sourceHandler.isDataServer()) { // Look for RS eligible for assured for (ServerHandler handler : replicationServers.values()) for (ReplicationServerHandler handler : replicationServers.values()) { if (handler.getGroupId() == groupId) // No ack expected from a RS with different group id @@ -541,7 +553,7 @@ } // Look for DS eligible for assured for (ServerHandler handler : directoryServers.values()) for (DataServerHandler handler : directoryServers.values()) { // Don't forward the change to the server that just sent it if (handler == sourceHandler) @@ -636,7 +648,7 @@ (generationId == sourceHandler.getGenerationId())) // Ignore assured updates from wrong generationId servers { if (sourceHandler.isLDAPserver()) if (sourceHandler.isDataServer()) { if (safeDataLevel == (byte) 1) { @@ -689,10 +701,10 @@ List<Short> expectedServers = new ArrayList<Short>(); if (interestedInAcks) { if (sourceHandler.isLDAPserver()) if (sourceHandler.isDataServer()) { // Look for RS eligible for assured for (ServerHandler handler : replicationServers.values()) for (ReplicationServerHandler handler : replicationServers.values()) { if (handler.getGroupId() == groupId) // No ack expected from a RS with different group id @@ -879,7 +891,7 @@ origServer.incrementAssuredSrReceivedUpdatesTimeout(); } else { if (origServer.isLDAPserver()) if (origServer.isDataServer()) { origServer.incrementAssuredSdReceivedUpdatesTimeout(); } @@ -957,7 +969,7 @@ */ public void stopReplicationServers(Collection<String> replServers) { for (ServerHandler handler : replicationServers.values()) for (ReplicationServerHandler handler : replicationServers.values()) { if (replServers.contains(handler.getServerAddressURL())) stopServer(handler); @@ -970,13 +982,13 @@ public void stopAllServers() { // Close session with other replication servers for (ServerHandler serverHandler : replicationServers.values()) for (ReplicationServerHandler serverHandler : replicationServers.values()) { stopServer(serverHandler); } // Close session with other LDAP servers for (ServerHandler serverHandler : directoryServers.values()) for (DataServerHandler serverHandler : directoryServers.values()) { stopServer(serverHandler); } @@ -988,14 +1000,13 @@ * @param handler the DS we want to check * @return true if this is not a duplicate server */ public boolean checkForDuplicateDS(ServerHandler handler) public boolean checkForDuplicateDS(DataServerHandler handler) { ServerHandler oldHandler = directoryServers.get(handler.getServerId()); DataServerHandler oldHandler = directoryServers.get(handler.getServerId()); if (directoryServers.containsKey(handler.getServerId())) { // looks like two LDAP servers have the same serverId // log an error message and drop this connection. Message message = ERR_DUPLICATE_SERVER_ID.get( replicationServer.getMonitorInstanceName(), oldHandler.toString(), handler.toString(), handler.getServerId()); @@ -1012,6 +1023,12 @@ */ public void stopServer(ServerHandler handler) { if (debugEnabled()) TRACER.debugInfo( "In RS " + this.replicationServer.getMonitorInstanceName() + " domain=" + this + " stopServer(SH)" + handler.getMonitorInstanceName() + " " + stackTraceToSingleLineString(new Exception())); /* * We must prevent deadlock on replication server domain lock, when for * instance this code is called from dying ServerReader but also dying @@ -1020,14 +1037,103 @@ * or not (is already being processed or not). */ if (!handler.engageShutdown()) // Only do this once (prevent other thread to enter here again) // Only do this once (prevent other thread to enter here again) { if (debugEnabled()) TRACER.debugInfo( "In RS " + this.replicationServer.getMonitorInstanceName() + " for " + baseDn + " " + " stopServer " + handler.getMonitorInstanceName()); try { try { // Acquire lock on domain (see more details in comment of start() // method of ServerHandler) lock(); } catch (InterruptedException ex) { // Try doing job anyway... } if (handler.isReplicationServer()) { if (replicationServers.containsValue(handler)) { unregisterServerHandler(handler); handler.shutdown(); // Check if generation id has to be resetted mayResetGenerationId(); // Warn our DSs that a RS or DS has quit (does not use this // handler as already removed from list) buildAndSendTopoInfoToDSs(null); } } else { if (directoryServers.containsValue(handler)) { // If this is the last DS for the domain, // shutdown the status analyzer if (directoryServers.size() == 1) { if (debugEnabled()) TRACER.debugInfo("In " + replicationServer.getMonitorInstanceName() + " remote server " + handler.getMonitorInstanceName() + " is the last DS to be stopped: stopping status analyzer"); stopStatusAnalyzer(); } unregisterServerHandler(handler); handler.shutdown(); // Check if generation id has to be resetted mayResetGenerationId(); // Update the remote replication servers with our list // of connected LDAP servers buildAndSendTopoInfoToRSs(); // Warn our DSs that a RS or DS has quit (does not use this // handler as already removed from list) buildAndSendTopoInfoToDSs(null); } else if (otherHandlers.contains(handler)) { unRegisterHandler(handler); handler.shutdown(); } } } catch(Exception e) { logError(Message.raw(Category.SYNC, Severity.NOTICE, stackTraceToSingleLineString(e))); } finally { release(); } } } /** * Stop the handler. * @param handler The handler to stop. */ public void stopServer(MessageHandler handler) { if (debugEnabled()) TRACER.debugInfo( "In RS " + this.replicationServer.getMonitorInstanceName() + " domain=" + this + " stopServer(MH)" + handler.getMonitorInstanceName() + " " + stackTraceToSingleLineString(new Exception())); /* * We must prevent deadlock on replication server domain lock, when for * instance this code is called from dying ServerReader but also dying * ServerWriter at the same time, or from a thread that wants to shut down * the handler. So use a thread safe flag to know if the job must be done * or not (is already being processed or not). */ if (!handler.engageShutdown()) // Only do this once (prevent other thread to enter here again) { try { // Acquire lock on domain (see more details in comment of start() @@ -1037,57 +1143,40 @@ { // Try doing job anyway... } if (handler.isReplicationServer()) if (otherHandlers.contains(handler)) { if (replicationServers.containsValue(handler)) { replicationServers.remove(handler.getServerId()); handler.shutdown(); // Check if generation id has to be resetted mayResetGenerationId(); // Warn our DSs that a RS or DS has quit (does not use this // handler as already removed from list) sendTopoInfoToDSs(null); } } else { if (directoryServers.containsValue(handler)) { // If this is the last DS for the domain, shutdown the status analyzer if (directoryServers.size() == 1) { if (debugEnabled()) TRACER.debugInfo("In " + replicationServer.getMonitorInstanceName() + " remote server " + handler.getMonitorInstanceName() + " is the last DS to be stopped: stopping status analyzer"); stopStatusAnalyzer(); } directoryServers.remove(handler.getServerId()); handler.shutdown(); // Check if generation id has to be resetted mayResetGenerationId(); // Update the remote replication servers with our list // of connected LDAP servers sendTopoInfoToRSs(); // Warn our DSs that a RS or DS has quit (does not use this // handler as already removed from list) sendTopoInfoToDSs(null); } unRegisterHandler(handler); handler.shutdown(); } } release(); } release(); /** * Unregister this handler from the list of handlers registered to this * domain. * @param handler the provided handler to unregister. */ protected void unregisterServerHandler(ServerHandler handler) { if (handler.isReplicationServer()) { replicationServers.remove(handler.getServerId()); } else { directoryServers.remove(handler.getServerId()); } } /** * Resets the generationId for this domain if there is no LDAP * server currently connected and if the generationId has never * been saved. * This method resets the generationId for this domain if there is no LDAP * server currently connected in the whole topology on this domain and * if the generationId has never been saved. * * - test emtpyness of directoryServers list * - traverse replicationServers list and test for each if DS are connected * So it strongly relies on the directoryServers list */ protected void mayResetGenerationId() { @@ -1104,7 +1193,7 @@ boolean lDAPServersConnectedInTheTopology = false; if (directoryServers.isEmpty()) { for (ServerHandler rsh : replicationServers.values()) for (ReplicationServerHandler rsh : replicationServers.values()) { if (generationId != rsh.getGenerationId()) { @@ -1149,14 +1238,16 @@ } /** * Checks that a RS is not already connected. * * @param handler the RS we want to check * @return true if this is not a duplicate server * Checks that a remote RS is not already connected to this hosting RS. * @param handler The handler for the remote RS. * @return flag specifying whether the remote RS is already connected. * @throws DirectoryException when a problem occurs. */ public boolean checkForDuplicateRS(ServerHandler handler) public boolean checkForDuplicateRS(ReplicationServerHandler handler) throws DirectoryException { ServerHandler oldHandler = replicationServers.get(handler.getServerId()); ReplicationServerHandler oldHandler = replicationServers.get(handler.getServerId()); if ((oldHandler != null)) { if (oldHandler.getServerAddressURL().equals( @@ -1166,7 +1257,9 @@ // have been sent at about the same time and 2 connections // have been established. // Silently drop this connection. } else return false; } else { // looks like two replication servers have the same serverId // log an error message and drop this connection. @@ -1174,9 +1267,8 @@ replicationServer.getMonitorInstanceName(), oldHandler. getServerAddressURL(), handler.getServerAddressURL(), handler.getServerId()); logError(message); throw new DirectoryException(ResultCode.OTHER, message); } return false; } return true; } @@ -1223,7 +1315,7 @@ { LinkedHashSet<String> mySet = new LinkedHashSet<String>(); for (ServerHandler handler : replicationServers.values()) for (ReplicationServerHandler handler : replicationServers.values()) { mySet.add(handler.getServerAddressURL()); } @@ -1232,7 +1324,9 @@ } /** * Return a Set containing the servers known by this replicationServer. * Return a set containing the server that produced update and known by * this replicationServer from all over the topology, * whatever directly connected of connected to another RS. * @return a set containing the servers known by this replicationServer. */ public Set<Short> getServers() @@ -1250,7 +1344,7 @@ { List<String> mySet = new ArrayList<String>(0); for (ServerHandler handler : directoryServers.values()) for (DataServerHandler handler : directoryServers.values()) { mySet.add(String.valueOf(handler.getServerId())); } @@ -1348,7 +1442,7 @@ { // Send to all replication servers with a least one remote // server connected for (ServerHandler rsh : replicationServers.values()) for (ReplicationServerHandler rsh : replicationServers.values()) { if (rsh.hasRemoteLDAPServers()) { @@ -1358,7 +1452,7 @@ } // Sends to all connected LDAP servers for (ServerHandler destinationHandler : directoryServers.values()) for (DataServerHandler destinationHandler : directoryServers.values()) { // Don't loop on the sender if (destinationHandler == senderHandler) @@ -1368,7 +1462,7 @@ } else { // Destination is one server ServerHandler destinationHandler = DataServerHandler destinationHandler = directoryServers.get(msg.getDestination()); if (destinationHandler != null) { @@ -1378,9 +1472,9 @@ // the targeted server is NOT connected // Let's search for THE changelog server that MAY // have the targeted server connected. if (senderHandler.isLDAPserver()) if (senderHandler.isDataServer()) { for (ServerHandler h : replicationServers.values()) for (ReplicationServerHandler h : replicationServers.values()) { // Send to all replication servers with a least one remote // server connected @@ -1421,7 +1515,7 @@ // build the full list of all servers in the topology // and send back a MonitorMsg with the full list of all the servers // in the topology. if (senderHandler.isLDAPserver()) if (senderHandler.isDataServer()) { MonitorMsg returnMsg = new MonitorMsg(msg.getDestination(), msg.getsenderID()); @@ -1481,7 +1575,7 @@ // from the states stored in the serverHandler. // - the server state // - the older missing change for (ServerHandler lsh : this.directoryServers.values()) for (DataServerHandler lsh : this.directoryServers.values()) { monitorMsg.setServerState( lsh.getServerId(), @@ -1491,7 +1585,7 @@ } // Same for the connected RS for (ServerHandler rsh : this.replicationServers.values()) for (ReplicationServerHandler rsh : this.replicationServers.values()) { monitorMsg.setServerState( rsh.getServerId(), @@ -1657,12 +1751,12 @@ */ public void checkAllSaturation() throws IOException { for (ServerHandler handler : replicationServers.values()) for (ReplicationServerHandler handler : replicationServers.values()) { handler.checkWindow(); } for (ServerHandler handler : directoryServers.values()) for (DataServerHandler handler : directoryServers.values()) { handler.checkWindow(); } @@ -1675,15 +1769,15 @@ * @return true if the server can restart sending changes. * false if the server can't restart sending changes. */ public boolean restartAfterSaturation(ServerHandler sourceHandler) public boolean restartAfterSaturation(MessageHandler sourceHandler) { for (ServerHandler handler : replicationServers.values()) for (MessageHandler handler : replicationServers.values()) { if (!handler.restartAfterSaturation(sourceHandler)) return false; } for (ServerHandler handler : directoryServers.values()) for (MessageHandler handler : directoryServers.values()) { if (!handler.restartAfterSaturation(sourceHandler)) return false; @@ -1698,9 +1792,9 @@ * @param notThisOne If not null, the topology message will not be sent to * this passed server. */ public void sendTopoInfoToDSs(ServerHandler notThisOne) public void buildAndSendTopoInfoToDSs(ServerHandler notThisOne) { for (ServerHandler handler : directoryServers.values()) for (DataServerHandler handler : directoryServers.values()) { if ((notThisOne == null) || // All DSs requested ((notThisOne != null) && (handler != notThisOne))) @@ -1725,10 +1819,10 @@ * Send a TopologyMsg to all the connected replication servers * in order to let them know our connected LDAP servers. */ public void sendTopoInfoToRSs() public void buildAndSendTopoInfoToRSs() { TopologyMsg topoMsg = createTopologyMsgForRS(); for (ServerHandler handler : replicationServers.values()) for (ReplicationServerHandler handler : replicationServers.values()) { try { @@ -1755,7 +1849,7 @@ List<DSInfo> dsInfos = new ArrayList<DSInfo>(); // Go through every DSs for (ServerHandler serverHandler : directoryServers.values()) for (DataServerHandler serverHandler : directoryServers.values()) { dsInfos.add(serverHandler.toDSInfo()); } @@ -1785,7 +1879,7 @@ List<RSInfo> rsInfos = new ArrayList<RSInfo>(); // Go through every DSs (except recipient of msg) for (ServerHandler serverHandler : directoryServers.values()) for (DataServerHandler serverHandler : directoryServers.values()) { if (serverHandler.getServerId() == destDsId) continue; @@ -1799,7 +1893,7 @@ // Go through every peer RSs (and get their connected DSs), also add info // for RSs for (ServerHandler serverHandler : replicationServers.values()) for (ReplicationServerHandler serverHandler : replicationServers.values()) { // Put RS info rsInfos.add(serverHandler.toRSInfo()); @@ -1840,12 +1934,6 @@ synchronized public long setGenerationId(long generationId, boolean savedStatus) { if (debugEnabled()) TRACER.debugInfo( "In " + this.replicationServer.getMonitorInstanceName() + " baseDN=" + baseDn + " RCache.set GenerationId=" + generationId); long oldGenerationId = this.generationId; if (this.generationId != generationId) @@ -1916,7 +2004,7 @@ // After we'll have sent the message , the remote RS will adopt // the new genId rsHandler.setGenerationId(newGenId); if (senderHandler.isLDAPserver()) if (senderHandler.isDataServer()) { rsHandler.forwardGenerationIdToRS(genIdMsg); } @@ -1929,7 +2017,7 @@ // Change status of the connected DSs according to the requested new // reference generation id for (ServerHandler dsHandler : directoryServers.values()) for (DataServerHandler dsHandler : directoryServers.values()) { try { @@ -1948,8 +2036,8 @@ // (consecutive to reset gen id message), we prefer advertising once for // all after changes (less packet sent), here at the end of the reset msg // treatment. sendTopoInfoToDSs(null); sendTopoInfoToRSs(); buildAndSendTopoInfoToDSs(null); buildAndSendTopoInfoToRSs(); Message message = NOTE_RESET_GENERATION_ID.get(baseDn.toString(), Long.toString(newGenId)); @@ -1964,7 +2052,7 @@ * that changed his status. * @param csMsg The message containing the new status */ public void processNewStatus(ServerHandler senderHandler, public void processNewStatus(DataServerHandler senderHandler, ChangeStatusMsg csMsg) { if (debugEnabled()) @@ -1995,8 +2083,8 @@ } // Update every peers (RS/DS) with topology changes sendTopoInfoToDSs(senderHandler); sendTopoInfoToRSs(); buildAndSendTopoInfoToDSs(senderHandler); buildAndSendTopoInfoToRSs(); Message message = NOTE_DIRECTORY_SERVER_CHANGED_STATUS.get( Short.toString(senderHandler.getServerId()), @@ -2014,7 +2102,7 @@ * @param event The event to be used for new status computation * @return True if we have been interrupted (must stop), false otherwise */ public boolean changeStatusFromStatusAnalyzer(ServerHandler serverHandler, public boolean changeStatusFromStatusAnalyzer(DataServerHandler serverHandler, StatusMachineEvent event) { try @@ -2066,8 +2154,8 @@ } // Update every peers (RS/DS) with topology changes sendTopoInfoToDSs(serverHandler); sendTopoInfoToRSs(); buildAndSendTopoInfoToDSs(serverHandler); buildAndSendTopoInfoToRSs(); release(); return false; @@ -2169,10 +2257,12 @@ * @param allowResetGenId True for allowing to reset the generation id ( * when called after initial handshake) * @throws IOException If an error occurred. * @throws DirectoryException If an error occurred. */ public void receiveTopoInfoFromRS(TopologyMsg topoMsg, ServerHandler handler, public void receiveTopoInfoFromRS(TopologyMsg topoMsg, ReplicationServerHandler handler, boolean allowResetGenId) throws IOException throws IOException, DirectoryException { if (debugEnabled()) { @@ -2186,7 +2276,8 @@ { // Acquire lock on domain (see more details in comment of start() method // of ServerHandler) lock(); if (!hasLock()) lock(); } catch (InterruptedException ex) { // Try doing job anyway... @@ -2228,7 +2319,7 @@ * Sends the currently known topology information to every connected * DS we have. */ sendTopoInfoToDSs(null); buildAndSendTopoInfoToDSs(null); release(); } @@ -2441,7 +2532,7 @@ { // this is the latency of the remote RSi regarding another RSj // let's update the latency of the LSes connected to RSj ServerHandler rsjHdr = replicationServers.get(rsid); ReplicationServerHandler rsjHdr = replicationServers.get(rsid); if (rsjHdr != null) { for (short remotelsid : rsjHdr.getConnectedDirectoryServerIds()) @@ -2496,7 +2587,7 @@ * Get the map of connected DSs. * @return The map of connected DSs */ public Map<Short, ServerHandler> getConnectedDSs() public Map<Short, DataServerHandler> getConnectedDSs() { return directoryServers; } @@ -2505,7 +2596,7 @@ * Get the map of connected RSs. * @return The map of connected RSs */ public Map<Short, ServerHandler> getConnectedRSs() public Map<Short, ReplicationServerHandler> getConnectedRSs() { return replicationServers; } @@ -2708,5 +2799,140 @@ return attributes; } } /** * Register in the domain an handler that subscribes to changes. * @param handler the provided subscribing handler. */ public void registerHandler(MessageHandler handler) { this.otherHandlers.add(handler); } /** * Unregister from the domain an handler. * @param handler the provided unsubscribing handler. * @return Whether this handler has been unregistered with success. */ public boolean unRegisterHandler(MessageHandler handler) { return this.otherHandlers.remove(handler); } /** * Return the state that contain for each server the time of eligibility. * @return the state. */ public ServerState getHeartbeatState() { // TODO:ECL Eligility must be supported return this.getDbServerState(); } /** * Computes the change number eligible to the ECL. * @return null if the domain does not play in eligibility. */ public ChangeNumber computeEligibleCN() { ChangeNumber elligibleCN = null; ServerState heartbeatState = getHeartbeatState(); if (heartbeatState==null) return null; // compute elligible CN ServerState hbState = heartbeatState.duplicate(); Iterator<Short> it = hbState.iterator(); while (it.hasNext()) { short sid = it.next(); ChangeNumber storedCN = hbState.getMaxChangeNumber(sid); // If the most recent UpdateMsg or CLHeartbeatMsg received is very old // then the server is considered down and not considered for eligibility if (TimeThread.getTime()-storedCN.getTime()>2000) { if (debugEnabled()) TRACER.debugInfo( "For RSD." + this.baseDn + " Server " + sid + " is not considered for eligibility ... potentially down"); continue; } if ((elligibleCN == null) || (storedCN.older(elligibleCN))) { elligibleCN = storedCN; } } if (debugEnabled()) TRACER.debugInfo( "For RSD." + this.baseDn + " ElligibleCN()=" + elligibleCN); return elligibleCN; } /** * Computes the eligible server state by minimizing the dbServerState and the * elligibleCN. * @return The computed eligible server state. */ public ServerState getCLElligibleState() { // ChangeNumber elligibleCN = computeEligibleCN(); ServerState res = new ServerState(); ServerState dbState = this.getDbServerState(); res = dbState; /* TODO:ECL Eligibility is not yet implemented Iterator<Short> it = dbState.iterator(); while (it.hasNext()) { Short sid = it.next(); DbHandler h = sourceDbHandlers.get(sid); ChangeNumber dbCN = dbState.getMaxChangeNumber(sid); try { if ((elligibleCN!=null)&&(elligibleCN.older(dbCN))) { // some CN exist in the db newer than elligible CN ReplicationIterator ri = h.generateIterator(elligibleCN); ChangeNumber newCN = ri.getCurrentCN(); res.update(newCN); ri.releaseCursor(); } else { // no CN exist in the db newer than elligible CN res.update(dbCN); } } catch(Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } } */ if (debugEnabled()) TRACER.debugInfo("In " + this.getName() + " getCLElligibleState returns:" + res); return res; } /** * Returns the start state of the domain, made of the first (oldest) * change stored for each serverId. * @return t start state of the domain. */ public ServerState getStartState() { ServerState domainStartState = new ServerState(); Iterator<Short> it = this.getDbServerState().iterator(); for (DbHandler dbHandler : sourceDbHandlers.values()) { domainStartState.update(dbHandler.getFirstChange()); } return domainStartState; } } opends/src/server/org/opends/server/replication/server/ReplicationServerHandler.java
New file @@ -0,0 +1,791 @@ /* * 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-2009 Sun Microsystems, Inc. */ 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.util.StaticUtils.stackTraceToSingleLineString; import java.io.IOException; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.opends.messages.Message; import org.opends.server.replication.common.DSInfo; import org.opends.server.replication.common.RSInfo; import org.opends.server.replication.common.ServerState; import org.opends.server.replication.common.ServerStatus; import org.opends.server.replication.protocol.ProtocolSession; import org.opends.server.replication.protocol.ProtocolVersion; import org.opends.server.replication.protocol.ReplServerStartMsg; import org.opends.server.replication.protocol.ReplicationMsg; import org.opends.server.replication.protocol.TopologyMsg; 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 replication server. */ public class ReplicationServerHandler extends ServerHandler { /* * Properties filled only if remote server is a RS */ private String serverAddressURL; /** * this collection will contain as many elements as there are * LDAP servers connected to the remote replication server. */ private final Map<Short, LightweightServerHandler> remoteDirectoryServers = new ConcurrentHashMap<Short, LightweightServerHandler>(); /** * Starts this handler based on a start message received from remote server. * @param inReplServerStartMsg The start msg provided by the remote server. * @return Whether the remote server requires encryption or not. * @throws DirectoryException When a problem occurs. */ public boolean processStartFromRemote(ReplServerStartMsg inReplServerStartMsg) throws DirectoryException { try { protocolVersion = ProtocolVersion.minWithCurrent( inReplServerStartMsg.getVersion()); generationId = inReplServerStartMsg.getGenerationId(); serverId = inReplServerStartMsg.getServerId(); serverURL = inReplServerStartMsg.getServerURL(); int separator = serverURL.lastIndexOf(':'); serverAddressURL = session.getRemoteAddress() + ":" + serverURL.substring(separator + 1); setServiceIdAndDomain(inReplServerStartMsg.getBaseDn()); setInitialServerState(inReplServerStartMsg.getServerState()); setSendWindowSize(inReplServerStartMsg.getWindowSize()); if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { // We support connection from a V1 RS // Only V2 protocol has the group id in repl server start message this.groupId = inReplServerStartMsg.getGroupId(); } oldGenerationId = -100; // Duplicate server ? if (!replicationServerDomain.checkForDuplicateRS(this)) { abortStart(null); return false; } } catch(Exception e) { Message message = Message.raw(e.getLocalizedMessage()); throw new DirectoryException(ResultCode.OTHER, message); } return inReplServerStartMsg.getSSLEncryption(); } /** * Creates a new handler object to a remote replication server. * @param session The session with the remote RS. * @param queueSize The queue size to manage updates to that RS. * @param replicationServerURL The hosting local RS URL. * @param replicationServerId The hosting local RS serverId. * @param replicationServer The hosting local RS object. * @param rcvWindowSize The receiving window size. */ public ReplicationServerHandler( ProtocolSession session, int queueSize, String replicationServerURL, short replicationServerId, ReplicationServer replicationServer, int rcvWindowSize) { super(session, queueSize, replicationServerURL, replicationServerId, replicationServer, rcvWindowSize); } /** * Connect the hosting RS to the RS represented by THIS handler * on an outgoing connection. * @param serviceId The serviceId (usually baseDn). * @param sslEncryption The sslEncryption requested to the remote RS. * @throws DirectoryException when an error occurs. */ public void connect(String serviceId, boolean sslEncryption) throws DirectoryException { // // the encryption we will request to the peer as we are the session creator this.initSslEncryption = sslEncryption; setServiceIdAndDomain(serviceId); localGenerationId = replicationServerDomain.getGenerationId(); oldGenerationId = localGenerationId; try { // lockDomain(false); // notimeout // we are the initiator and decides of the encryption boolean sessionInitiatorSSLEncryption = this.initSslEncryption; // Send start ReplServerStartMsg outReplServerStartMsg = sendStartToRemote((short)-1); // Wait answer ReplicationMsg msg = session.receive(); // Reject bad responses if (!(msg instanceof ReplServerStartMsg)) { Message message = ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE.get( msg.getClass().getCanonicalName(), "ReplServerStartMsg"); abortStart(message); return; } // Process hello from remote processStartFromRemote((ReplServerStartMsg)msg); // Log logStartHandshakeSNDandRCV(outReplServerStartMsg,(ReplServerStartMsg)msg); // Until here session is encrypted then it depends on the negociation // The session initiator decides whether to use SSL. if (!sessionInitiatorSSLEncryption) session.stopEncryption(); if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { // Only protocol version above V1 has a phase 2 handshake // NOW PROCEDE WITH SECOND PHASE OF HANDSHAKE: // TopologyMsg then TopologyMsg (with a RS) // Send our own TopologyMsg to remote RS TopologyMsg outTopoMsg = sendTopoToRemoteRS(); // wait and process Topo from remote RS TopologyMsg inTopoMsg = waitAndProcessTopoFromRemoteRS(); logTopoHandshakeSNDandRCV(outTopoMsg, inTopoMsg); // FIXME: i think this should be done for all protocol version !! // not only those > V1 registerIntoDomain(); // Process TopologyMsg sent by remote RS: store matching new info // (this will also warn our connected DSs of the new received info) replicationServerDomain.receiveTopoInfoFromRS(inTopoMsg, this, false); } super.finalizeStart(); } catch(IOException ioe) { // FIXME receive } // catch(DirectoryException de) //{ already logged // catch(Exception e) { // FIXME more detailed exceptions } finally { // Release domain if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); } } /** * Starts the handler from a remote ReplServerStart message received from * the remote replication server. * @param inReplServerStartMsg The provided ReplServerStart message received. */ public void startFromRemoteRS(ReplServerStartMsg inReplServerStartMsg) { // localGenerationId = -1; oldGenerationId = -100; try { // Process start from remote boolean sessionInitiatorSSLEncryption = processStartFromRemote(inReplServerStartMsg); // lock with timeout lockDomain(true); short reqVersion = -1; if (protocolVersion == ProtocolVersion.REPLICATION_PROTOCOL_V1) { // We support connection from a V1 RS, send PDU with V1 form reqVersion = ProtocolVersion.REPLICATION_PROTOCOL_V1; } // send start to remote ReplServerStartMsg outReplServerStartMsg = sendStartToRemote(reqVersion); // log logStartHandshakeRCVandSND(inReplServerStartMsg, outReplServerStartMsg); // until here session is encrypted then it depends on the negociation // The session initiator decides whether to use SSL. if (!sessionInitiatorSSLEncryption) session.stopEncryption(); TopologyMsg inTopoMsg = null; if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { // Only protocol version above V1 has a phase 2 handshake // NOW PROCEDE WITH SECOND PHASE OF HANDSHAKE: // TopologyMsg then TopologyMsg (with a RS) // wait and process Topo from remote RS inTopoMsg = waitAndProcessTopoFromRemoteRS(); // send our own TopologyMsg to remote RS TopologyMsg outTopoMsg = sendTopoToRemoteRS(); // log logTopoHandshakeRCVandSND(inTopoMsg, outTopoMsg); } else { // Terminate connection from a V1 RS // if the remote RS and the local RS have the same genID // then it's ok and nothing else to do if (generationId == localGenerationId) { if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + this + " RS V1 with serverID=" + serverId + " is connected with the right generation ID"); } } else { if (localGenerationId > 0) { // if the local RS is initialized if (generationId > 0) { // if the remote RS is initialized if (generationId != localGenerationId) { // if the 2 RS have different generationID if (replicationServerDomain.getGenerationIdSavedStatus()) { // if the present RS has received changes regarding its // gen ID and so won't change without a reset // then we are just degrading the peer. Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( getServiceId(), Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } else { // The present RS has never received changes regarding its // gen ID. // // Example case: // - we are in RS1 // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2) // - RS1 has genId1 from LS1 /genId1 comes from data in // suffix // - we are in RS1 and we receive a START msg from RS2 // - Each RS keeps its genID / is degraded and when LS2 // will be populated from LS1 everything will become ok. // // Issue: // FIXME : Would it be a good idea in some cases to just // set the gen ID received from the peer RS // specially if the peer has a non null state and // we have a nul state ? // replicationServerDomain. // setGenerationId(generationId, false); Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( getServiceId(), Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } } } else { // The remote RS has no genId. We don't change anything for the // current RS. } } else { // The local RS is not initialized - take the one received oldGenerationId = replicationServerDomain.setGenerationId(generationId, false); } } // Note: the supported scenario for V1->V2 upgrade is to upgrade 1 by 1 // all the servers of the topology. We prefer not not send a TopologyMsg // for giving partial/false information to the V2 servers as for // instance we don't have the connected DS of the V1 RS...When the V1 // RS will be upgraded in his turn, topo info will be sent and accurate. // That way, there is no risk to have false/incomplete information in // other servers. } registerIntoDomain(); // Process TopologyMsg sent by remote RS: store matching new info // (this will also warn our connected DSs of the new received info) if (inTopoMsg!=null) replicationServerDomain.receiveTopoInfoFromRS(inTopoMsg, this, false); super.finalizeStart(); } catch(DirectoryException de) { abortStart(de.getMessageObject()); } catch(Exception e) { abortStart(Message.raw(e.getLocalizedMessage())); } finally { if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); } } /** * Registers this handler into its related domain and notifies the domain. */ private void registerIntoDomain() { // Alright, connected with new RS (either outgoing or incoming // connection): store handler. Map<Short, ReplicationServerHandler> connectedRSs = replicationServerDomain.getConnectedRSs(); connectedRSs.put(serverId, this); } /** * Create and send the topologyMsg to the remote replication server. * @return the topologyMsg sent. */ private TopologyMsg sendTopoToRemoteRS() throws IOException { TopologyMsg outTopoMsg = replicationServerDomain.createTopologyMsgForRS(); session.publish(outTopoMsg); return outTopoMsg; } /** * Wait receiving the TopologyMsg from the remote RS and process it. * @return the topologyMsg received. * @throws DirectoryException * @throws IOException */ private TopologyMsg waitAndProcessTopoFromRemoteRS() throws DirectoryException, IOException { ReplicationMsg msg = null; try { msg = session.receive(); } catch(Exception e) { Message message = Message.raw(e.getLocalizedMessage()); throw new DirectoryException(ResultCode.OTHER, message); } if (!(msg instanceof TopologyMsg)) { Message message = ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE.get( msg.getClass().getCanonicalName(), "TopologyMsg"); abortStart(message); } // Remote RS sent his topo msg TopologyMsg inTopoMsg = (TopologyMsg) msg; // CONNECTION WITH A RS // if the remote RS and the local RS have the same genID // then it's ok and nothing else to do if (generationId == localGenerationId) { if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " RS with serverID=" + serverId + " is connected with the right generation ID, same as local =" + generationId); } } else { if (localGenerationId > 0) { // if the local RS is initialized if (generationId > 0) { // if the remote RS is initialized if (generationId != localGenerationId) { // if the 2 RS have different generationID if (replicationServerDomain.getGenerationIdSavedStatus()) { // if the present RS has received changes regarding its // gen ID and so won't change without a reset // then we are just degrading the peer. Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( getServiceId(), Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } else { // The present RS has never received changes regarding its // gen ID. // // Example case: // - we are in RS1 // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2) // - RS1 has genId1 from LS1 /genId1 comes from data in // suffix // - we are in RS1 and we receive a START msg from RS2 // - Each RS keeps its genID / is degraded and when LS2 // will be populated from LS1 everything will become ok. // // Issue: // FIXME : Would it be a good idea in some cases to just // set the gen ID received from the peer RS // specially if the peer has a non null state and // we have a nul state ? // replicationServerDomain. // setGenerationId(generationId, false); Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( getServiceId(), Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } } } else { // The remote RS has no genId. We don't change anything for the // current RS. } } 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.setGenerationId(generationId, false); } } return inTopoMsg; } /** * {@inheritDoc} */ @Override public boolean isDataServer() { return false; } /** * Add the DSinfos of the connected Directory Servers * to the List of DSInfo provided as a parameter. * * @param dsInfos The List of DSInfo that should be updated * with the DSInfo for the remoteDirectoryServers * connected to this ServerHandler. */ public void addDSInfos(List<DSInfo> dsInfos) { synchronized (remoteDirectoryServers) { for (LightweightServerHandler ls : remoteDirectoryServers.values()) { dsInfos.add(ls.toDSInfo()); } } } /** * Shutdown This ServerHandler. */ public void shutdown() { super.shutdown(); /* * Stop the remote LSHandler */ synchronized (remoteDirectoryServers) { for (LightweightServerHandler lsh : remoteDirectoryServers.values()) { lsh.stopHandler(); } remoteDirectoryServers.clear(); } } /** * Stores topology information received from a peer RS and that must be kept * in RS handler. * * @param topoMsg The received topology message */ public void receiveTopoInfoFromRS(TopologyMsg topoMsg) { // Store info for remote RS List<RSInfo> rsInfos = topoMsg.getRsList(); // List should only contain RS info for sender RSInfo rsInfo = rsInfos.get(0); generationId = rsInfo.getGenerationId(); groupId = rsInfo.getGroupId(); /** * Store info for DSs connected to the peer RS */ List<DSInfo> dsInfos = topoMsg.getDsList(); synchronized (remoteDirectoryServers) { // Removes the existing structures for (LightweightServerHandler lsh : remoteDirectoryServers.values()) { lsh.stopHandler(); } remoteDirectoryServers.clear(); // Creates the new structure according to the message received. for (DSInfo dsInfo : dsInfos) { LightweightServerHandler lsh = new LightweightServerHandler(this, serverId, dsInfo.getDsId(), dsInfo.getGenerationId(), dsInfo.getGroupId(), dsInfo.getStatus(), dsInfo.getRefUrls(), dsInfo.isAssured(), dsInfo.getAssuredMode(), dsInfo.getSafeDataLevel()); lsh.startHandler(); remoteDirectoryServers.put(lsh.getServerId(), lsh); } } } /** * When this handler is connected to a replication server, specifies if * a wanted server is connected to this replication server. * * @param wantedServer The server we want to know if it is connected * to the replication server represented by this handler. * @return boolean True is the wanted server is connected to the server * represented by this handler. */ public boolean isRemoteLDAPServer(short wantedServer) { synchronized (remoteDirectoryServers) { for (LightweightServerHandler server : remoteDirectoryServers.values()) { if (wantedServer == server.getServerId()) { return true; } } return false; } } /** * When the handler is connected to a replication server, specifies the * replication server has remote LDAP servers connected to it. * * @return boolean True is the replication server has remote LDAP servers * connected to it. */ public boolean hasRemoteLDAPServers() { synchronized (remoteDirectoryServers) { return !remoteDirectoryServers.isEmpty(); } } /** * Return a Set containing the servers known by this replicationServer. * @return a set containing the servers known by this replicationServer. */ public Set<Short> getConnectedDirectoryServerIds() { synchronized (remoteDirectoryServers) { return remoteDirectoryServers.keySet(); } } /** * {@inheritDoc} */ @Override public String getMonitorInstanceName() { String str = serverURL + " " + String.valueOf(serverId); return "Connected Replication Server " + str + ",cn=" + replicationServerDomain.getMonitorInstanceName(); } /** * 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 RS ones attributes.add(Attributes.create("Replication-Server", serverURL)); try { MonitorData md; md = replicationServerDomain.computeMonitorData(); // Missing changes long missingChanges = md.getMissingChangesRS(serverId); attributes.add(Attributes.create("missing-changes", String .valueOf(missingChanges))); /* get the Server State */ AttributeBuilder builder = new AttributeBuilder("server-state"); ServerState state = md.getRSStates(serverId); if (state != null) { for (String str : state.toStringSet()) { builder.add(str); } attributes.add(builder.toAttribute()); } } catch (Exception e) { Message message = ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e)); // We failed retrieving the monitor data. attributes.add(Attributes.create("error", message.toString())); } return attributes; } /** * {@inheritDoc} */ @Override public String toString() { String localString; if (serverId != 0) { localString = "Replication Server "; localString += serverId + " " + serverURL + " " + getServiceId(); } else localString = "Unknown server"; return localString; } /** * Gets the status of the connected DS. * @return The status of the connected DS. */ public ServerStatus getStatus() { return ServerStatus.INVALID_STATUS; } /** * Retrieves the Address URL for this server handler. * * @return The Address URL for this server handler, * in the form of an IP address and port separated by a colon. */ public String getServerAddressURL() { return serverAddressURL; } } opends/src/server/org/opends/server/replication/server/SafeDataExpectedAcksInfo.java
@@ -85,7 +85,7 @@ * him with assured safe data mode, we double check here that the ack sender * is a RS to take the ack into account. */ if (ackingServer.isLDAPserver()) if (ackingServer.isDataServer()) { // Sanity check: this should never happen if (debugEnabled()) opends/src/server/org/opends/server/replication/server/ServerHandler.java
@@ -26,181 +26,217 @@ */ package org.opends.server.replication.server; import org.opends.messages.*; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.*; import static org.opends.server.replication.common.StatusMachine.*; import org.opends.server.loggers.debug.DebugTracer; import static org.opends.messages.ReplicationMessages.*; import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import java.io.IOException; import java.util.Date; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; import org.opends.messages.Message; import org.opends.server.admin.std.server.MonitorProviderCfg; import org.opends.server.api.MonitorProvider; import org.opends.server.config.ConfigException; import org.opends.server.core.DirectoryServer; import org.opends.server.replication.common.AssuredMode; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.DSInfo; import org.opends.server.replication.common.RSInfo; 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.replication.protocol.AckMsg; import org.opends.server.replication.protocol.ErrorMsg; import org.opends.server.replication.protocol.HeartbeatThread; import org.opends.server.replication.protocol.ProtocolSession; import org.opends.server.replication.protocol.ProtocolVersion; import org.opends.server.replication.protocol.ReplServerStartMsg; import org.opends.server.replication.protocol.ResetGenerationIdMsg; import org.opends.server.replication.protocol.RoutableMsg; import org.opends.server.replication.protocol.StartECLSessionMsg; import org.opends.server.replication.protocol.StartMsg; import org.opends.server.replication.protocol.StartSessionMsg; import org.opends.server.replication.protocol.TopologyMsg; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.protocol.WindowMsg; import org.opends.server.replication.protocol.WindowProbeMsg; 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.InitializationException; import org.opends.server.types.ResultCode; import org.opends.server.util.TimeThread; /** * This class defines a server handler, which handles all interaction with a * peer server (RS or DS). * This class defines a server handler : * - that is a MessageHandler (see this class for more details) * - that handles all interaction with a peer server (RS or DS). */ public class ServerHandler extends MonitorProvider<MonitorProviderCfg> public abstract class ServerHandler extends MessageHandler { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * Time during which the server will wait for existing thread to stop * during the shutdownWriter. */ private static final int SHUTDOWN_JOIN_TIMEOUT = 30000; /* * Properties, filled if remote server is either a DS or a RS /** * Close the session and log the provided error message * Log nothing if message is null. * @param providedSession The provided closing session. * @param providedMsg The provided error message. * @param handler The handler that manages that session. */ private short serverId; private ProtocolSession session; private final MsgQueue msgQueue = new MsgQueue(); private MsgQueue lateQueue = new MsgQueue(); private ReplicationServerDomain replicationServerDomain = null; private String serverURL; static protected void closeSession(ProtocolSession providedSession, Message providedMsg, ServerHandler handler) { if (providedMsg != null) { if (debugEnabled()) TRACER.debugInfo("In " + ((handler!=null)?handler.toString():"Replication Server") + " closing session with err=" + providedMsg.toString()); logError(providedMsg); } try { if (providedSession!=null) providedSession.close(); } catch (IOException ee) { // ignore } } // Number of update sent to the server private int outCount = 0; // Number of updates received from the server private int inCount = 0; /** * The serverId of the remote server. */ protected short serverId; /** * The session opened with the remote server. */ protected ProtocolSession session; // Number of updates received from the server in assured safe read mode private int assuredSrReceivedUpdates = 0; // Number of updates received from the server in assured safe read mode that // timed out private AtomicInteger assuredSrReceivedUpdatesTimeout = new AtomicInteger(); // Number of updates sent to the server in assured safe read mode private int assuredSrSentUpdates = 0; // Number of updates sent to the server in assured safe read mode that timed // out private AtomicInteger assuredSrSentUpdatesTimeout = new AtomicInteger(); // Number of updates received from the server in assured safe data mode private int assuredSdReceivedUpdates = 0; // Number of updates received from the server in assured safe data mode that // timed out private AtomicInteger assuredSdReceivedUpdatesTimeout = new AtomicInteger(); // Number of updates sent to the server in assured safe data mode private int assuredSdSentUpdates = 0; // Number of updates sent to the server in assured safe data mode that timed // out private AtomicInteger assuredSdSentUpdatesTimeout = new AtomicInteger(); /** * The serverURL of the remote server. */ protected String serverURL; /** * Number of updates received from the server in assured safe read mode. */ protected int assuredSrReceivedUpdates = 0; /** * Number of updates received from the server in assured safe read mode that * timed out. */ protected AtomicInteger assuredSrReceivedUpdatesTimeout = new AtomicInteger(); /** * Number of updates sent to the server in assured safe read mode. */ protected int assuredSrSentUpdates = 0; /** * Number of updates sent to the server in assured safe read mode that timed * out. */ protected AtomicInteger assuredSrSentUpdatesTimeout = new AtomicInteger(); /** // Number of updates received from the server in assured safe data mode. */ protected int assuredSdReceivedUpdates = 0; /** * Number of updates received from the server in assured safe data mode that * timed out. */ protected AtomicInteger assuredSdReceivedUpdatesTimeout = new AtomicInteger(); /** * Number of updates sent to the server in assured safe data mode. */ protected int assuredSdSentUpdates = 0; private int maxReceiveQueue = 0; private int maxSendQueue = 0; private int maxReceiveDelay = 0; private int maxSendDelay = 0; private int maxQueueSize = 5000; private int maxQueueBytesSize = maxQueueSize * 100; private int restartReceiveQueue; private int restartSendQueue; private int restartReceiveDelay; private int restartSendDelay; private boolean serverIsLDAPserver; private boolean following = false; private ServerState serverState; private boolean activeWriter = true; private ServerWriter writer = null; private String baseDn = null; /** * Number of updates sent to the server in assured safe data mode that timed * out. */ protected AtomicInteger assuredSdSentUpdatesTimeout = new AtomicInteger(); /** * The associated ServerWriter that sends messages to the remote server. */ protected ServerReader reader; /** * The associated ServerReader that receives messages from the remote server. */ protected ServerWriter writer = null; // window private int rcvWindow; private int rcvWindowSizeHalf; private int maxRcvWindow; private ServerReader reader; private Semaphore sendWindow; private int sendWindowSize; private boolean flowControl = false; // indicate that the server is // flow controlled and should // be stopped from sending messages. /** * Semaphore that the writer uses to control the flow to the remote server. */ protected Semaphore sendWindow; /** * The initial size of the sending window. */ int sendWindowSize; private int saturationCount = 0; private short replicationServerId; private short protocolVersion = -1; private long generationId = -1; // Group id of this remote server private byte groupId = (byte) -1; /* * Properties filled only if remote server is a DS */ // 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; /* * Properties filled only if remote server is a RS */ private String serverAddressURL; /** * When this Handler is related to a remote replication server * this collection will contain as many elements as there are * LDAP servers connected to the remote replication server. * The protocol version established with the remote server. */ private final Map<Short, LightweightServerHandler> directoryServers = new ConcurrentHashMap<Short, LightweightServerHandler>(); protected short protocolVersion = -1; /** * remote generation id. */ protected long generationId = -1; /** * The generation id of the hosting RS. */ protected long localGenerationId = -1; /** * The generation id before procesing a new start handshake. */ protected long oldGenerationId = -1; /** * Group id of this remote server. */ protected byte groupId = (byte) -1; /** * The SSL encryption provided by the creator/starter of this handler. */ protected boolean initSslEncryption; /** * The SSL encryption after the negociation with the peer. */ protected boolean sslEncryption; /** * The time in milliseconds between heartbeats from the replication * server. Zero means heartbeats are off. */ private long heartbeatInterval = 0; protected long heartbeatInterval = 0; /** * The thread that will send heartbeats. */ HeartbeatThread heartbeatThread = null; /** * Set when ServerWriter is stopping. */ private boolean shutdownWriter = false; protected boolean shutdownWriter = false; /** * Set when ServerHandler is stopping. */ private AtomicBoolean shuttingDown = new AtomicBoolean(false); /** * Creates a new server handler instance with the provided socket. * @@ -208,846 +244,142 @@ * communicate with the remote entity. * @param queueSize The maximum number of update that will be kept * in memory by this ServerHandler. * @param replicationServerURL The URL of the hosting replication server. * @param replicationServerId The serverId of the hosting replication server. * @param replicationServer The hosting replication server. * @param rcvWindowSize The window size to receive from the remote server. */ public ServerHandler(ProtocolSession session, int queueSize) public ServerHandler( ProtocolSession session, int queueSize, String replicationServerURL, short replicationServerId, ReplicationServer replicationServer, int rcvWindowSize) { super("Server Handler"); super(queueSize, replicationServerURL, replicationServerId, replicationServer); this.session = session; this.maxQueueSize = queueSize; this.maxQueueBytesSize = queueSize * 100; this.protocolVersion = ProtocolVersion.getCurrentVersion(); this.rcvWindowSizeHalf = rcvWindowSize / 2; this.maxRcvWindow = rcvWindowSize; this.rcvWindow = rcvWindowSize; } /** * Creates a DSInfo structure representing this remote DS. * @return The DSInfo structure representing this remote DS * Abort a start procedure currently establishing. * @param reason The provided reason. */ public DSInfo toDSInfo() protected void abortStart(Message reason) { DSInfo dsInfo = new DSInfo(serverId, replicationServerId, generationId, status, assuredFlag, assuredMode, safeDataLevel, groupId, refUrls); return dsInfo; } /** * Creates a RSInfo structure representing this remote RS. * @return The RSInfo structure representing this remote RS */ public RSInfo toRSInfo() { RSInfo rsInfo = new RSInfo(serverId, generationId, groupId); return rsInfo; } /** * Do the handshake with either the DS or RS and then create the reader and * writer thread. * * There are 2 possible handshake sequences: DS<->RS and RS<->RS. Each one are * divided into 2 logical consecutive phases (phase 1 and phase 2): * * DS<->RS (DS (always initiating connection) always sends first message): * ------- * * phase 1: * DS --- ServerStartMsg ---> RS * DS <--- ReplServerStartMsg --- RS * phase 2: * DS --- StartSessionMsg ---> RS * DS <--- TopologyMsg --- RS * * RS<->RS (RS initiating connection always sends first message): * ------- * * phase 1: * RS1 --- ReplServerStartMsg ---> RS2 * RS1 <--- ReplServerStartMsg --- RS2 * phase 2: * RS1 --- TopologyMsg ---> RS2 * RS1 <--- TopologyMsg --- RS2 * * @param baseDn baseDn of the ServerHandler when this is an outgoing conn. * null if this is an incoming connection (listen). * @param replicationServerId The identifier of the replicationServer that * creates this server handler. * @param replicationServerURL The URL of the replicationServer that creates * this server handler. * @param windowSize the window size that this server handler must use. * @param sslEncryption For outgoing connections indicates whether encryption * should be used after the exchange of start messages. * Ignored for incoming connections. * @param replicationServer the ReplicationServer that created this server * handler. */ public void start(String baseDn, short replicationServerId, String replicationServerURL, int windowSize, boolean sslEncryption, ReplicationServer replicationServer) { // The handshake phase must be done by blocking any access to structures // keeping info on connected servers, so that one can safely check for // pre-existence of a server, send a coherent snapshot of known topology // to peers, update the local view of the topology... // // For instance a kind of problem could be that while we connect with a // peer RS, a DS is connecting at the same time and we could publish the // connected DSs to the peer RS forgetting this last DS in the TopologyMsg. // // This method and every others that need to read/make changes to the // structures holding topology for the domain should: // - call ReplicationServerDomain.lock() // - read/modify structures // - call ReplicationServerDomain.release() // // More information is provided in comment of ReplicationServerDomain.lock() // If domain already exists, lock it until handshake is finished otherwise // it will be created and locked later in the method if (baseDn != null) // 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 if (session!=null) { ReplicationServerDomain rsd = replicationServer.getReplicationServerDomain(baseDn, false); if (rsd != null) try { try { rsd.lock(); } catch (InterruptedException ex) { // Thread interrupted, return. return; } session.publish( new ErrorMsg( replicationServerDomain.getReplicationServer().getServerId(), serverId, reason)); } catch(Exception e) { } closeSession(session, reason, this); } long oldGenerationId = -100; if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); if (debugEnabled()) TRACER.debugInfo("In " + replicationServer.getMonitorInstanceName() + " starts a new LS or RS " + ((baseDn == null) ? "incoming connection" : "outgoing connection")); this.replicationServerId = replicationServerId; rcvWindowSizeHalf = windowSize / 2; maxRcvWindow = windowSize; rcvWindow = windowSize; long localGenerationId = -1; ReplServerStartMsg outReplServerStartMsg = null; /** * This boolean prevents from logging a polluting error when connection\ * aborted from a DS that wanted only to perform handshake phase 1 in order * to determine the best suitable RS: * 1) -> ServerStartMsg * 2) <- ReplServerStartMsg * 3) connection closure */ boolean log_error_message = true; try // If generation id of domain was changed, set it back to old value // We may have changed it as it was -1 and we received a value >0 from // peer server and the last topo message sent may have failed being // sent: in that case retrieve old value of generation id for // replication server domain if (oldGenerationId != -100) { /* * PROCEDE WITH FIRST PHASE OF HANDSHAKE: * ServerStartMsg then ReplServerStartMsg (with a DS) * OR * ReplServerStartMsg then ReplServerStartMsg (with a RS) */ replicationServerDomain.setGenerationId(oldGenerationId, false); } } if (baseDn != null) // Outgoing connection /** * Check the protocol window and send WindowMsg if necessary. * * @throws IOException when the session becomes unavailable. */ public synchronized void checkWindow() throws IOException { if (rcvWindow < rcvWindowSizeHalf) { if (flowControl) { // This is an outgoing connection. Publish our start message. this.baseDn = baseDn; // Get or create the ReplicationServerDomain replicationServerDomain = replicationServer.getReplicationServerDomain(baseDn, true); if (!replicationServerDomain.hasLock()) if (replicationServerDomain.restartAfterSaturation(this)) { try { replicationServerDomain.lock(); } catch (InterruptedException ex) { // Thread interrupted, return. return; } flowControl = false; } localGenerationId = replicationServerDomain.getGenerationId(); } if (!flowControl) { WindowMsg msg = new WindowMsg(rcvWindowSizeHalf); session.publish(msg); rcvWindow += rcvWindowSizeHalf; } } } ServerState localServerState = replicationServerDomain.getDbServerState(); outReplServerStartMsg = new ReplServerStartMsg(replicationServerId, replicationServerURL, baseDn, windowSize, localServerState, protocolVersion, localGenerationId, sslEncryption, replicationServer.getGroupId(), replicationServerDomain. getReplicationServer().getDegradedStatusThreshold()); /** * Decrement the protocol window, then check if it is necessary * to send a WindowMsg and send it. * * @throws IOException when the session becomes unavailable. */ public synchronized void decAndCheckWindow() throws IOException { rcvWindow--; checkWindow(); } session.publish(outReplServerStartMsg); /** * Set the shut down flag to true and returns the previous value of the flag. * @return The previous value of the shut down flag */ public boolean engageShutdown() { // Use thread safe boolean return shuttingDown.getAndSet(true); } /** * Finalize the initialization, create reader, writer, heartbeat system * and monitoring system. * @throws DirectoryException When an exception is raised. */ protected void finalizeStart() throws DirectoryException { // FIXME:ECL We should refactor so that a SH always have a session if (session != null) { try { // Disable timeout for next communications session.setSoTimeout(0); } catch(Exception e) { } // Wait and process ServerStartMsg or ReplServerStartMsg ReplicationMsg msg = session.receive(); if (msg instanceof ServerStartMsg) { // The remote server is an LDAP Server. ServerStartMsg serverStartMsg = (ServerStartMsg) msg; generationId = serverStartMsg.getGenerationId(); protocolVersion = ProtocolVersion.minWithCurrent( serverStartMsg.getVersion()); serverId = serverStartMsg.getServerId(); serverURL = serverStartMsg.getServerURL(); this.baseDn = serverStartMsg.getBaseDn(); this.serverState = serverStartMsg.getServerState(); this.groupId = serverStartMsg.getGroupId(); maxReceiveDelay = serverStartMsg.getMaxReceiveDelay(); maxReceiveQueue = serverStartMsg.getMaxReceiveQueue(); maxSendDelay = serverStartMsg.getMaxSendDelay(); maxSendQueue = serverStartMsg.getMaxSendQueue(); heartbeatInterval = serverStartMsg.getHeartbeatInterval(); // The session initiator decides whether to use SSL. sslEncryption = serverStartMsg.getSSLEncryption(); if (maxReceiveQueue > 0) restartReceiveQueue = (maxReceiveQueue > 1000 ? maxReceiveQueue - 200 : maxReceiveQueue * 8 / 10); else restartReceiveQueue = 0; if (maxSendQueue > 0) restartSendQueue = (maxSendQueue > 1000 ? maxSendQueue - 200 : maxSendQueue * 8 / 10); else restartSendQueue = 0; if (maxReceiveDelay > 0) restartReceiveDelay = (maxReceiveDelay > 10 ? maxReceiveDelay - 1 : maxReceiveDelay); else restartReceiveDelay = 0; if (maxSendDelay > 0) restartSendDelay = (maxSendDelay > 10 ? maxSendDelay - 1 : maxSendDelay); else restartSendDelay = 0; if (heartbeatInterval < 0) { heartbeatInterval = 0; } serverIsLDAPserver = true; // Get or Create the ReplicationServerDomain replicationServerDomain = replicationServer.getReplicationServerDomain(this.baseDn, true); // 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 replicationServerDomain.waitDisconnection(serverStartMsg.getServerId()); if (!replicationServerDomain.hasLock()) { try { replicationServerDomain.lock(); } catch (InterruptedException ex) { // Thread interrupted, return. return; } } // Duplicate server ? if (!replicationServerDomain.checkForDuplicateDS(this)) { closeSession(null); if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); return; } localGenerationId = replicationServerDomain.getGenerationId(); ServerState localServerState = replicationServerDomain.getDbServerState(); // This an incoming connection. Publish our start message ReplServerStartMsg replServerStartMsg = new ReplServerStartMsg(replicationServerId, replicationServerURL, this.baseDn, windowSize, localServerState, protocolVersion, localGenerationId, sslEncryption, replicationServer.getGroupId(), replicationServerDomain. getReplicationServer().getDegradedStatusThreshold()); session.publish(replServerStartMsg); sendWindowSize = serverStartMsg.getWindowSize(); /* Until here session is encrypted then it depends on the negotiation */ if (!sslEncryption) { session.stopEncryption(); } if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ":" + "\nSH HANDSHAKE RECEIVED:\n" + serverStartMsg.toString() + "\nAND REPLIED:\n" + replServerStartMsg.toString()); } } else if (msg instanceof ReplServerStartMsg) { // The remote server is a replication server ReplServerStartMsg inReplServerStartMsg = (ReplServerStartMsg) msg; protocolVersion = ProtocolVersion.minWithCurrent( inReplServerStartMsg.getVersion()); generationId = inReplServerStartMsg.getGenerationId(); serverId = inReplServerStartMsg.getServerId(); serverURL = inReplServerStartMsg.getServerURL(); int separator = serverURL.lastIndexOf(':'); serverAddressURL = session.getRemoteAddress() + ":" + serverURL.substring(separator + 1); serverIsLDAPserver = false; if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { // We support connection from a V1 RS // Only V2 protocol has the group id in repl server start message this.groupId = inReplServerStartMsg.getGroupId(); } this.baseDn = inReplServerStartMsg.getBaseDn(); if (baseDn == null) // Reply to incoming RS { // Get or create the ReplicationServerDomain replicationServerDomain = replicationServer.getReplicationServerDomain(this.baseDn, true); if (!replicationServerDomain.hasLock()) { try { /** * Take the lock on the domain. * WARNING: Here we try to acquire the lock with a timeout. This * is for preventing a deadlock that may happen if there are cross * connection attempts (for same domain) from this replication * server and from a peer one: * Here is the scenario: * - RS1 connect thread takes the domain lock and starts * connection to RS2 * - at the same time RS2 connect thread takes his domain lock and * start connection to RS2 * - RS2 listen thread starts processing received * ReplServerStartMsg from RS1 and wants to acquire the lock on * the domain (here) but cannot as RS2 connect thread already has * it * - RS1 listen thread starts processing received * ReplServerStartMsg from RS2 and wants to acquire the lock on * the domain (here) but cannot as RS1 connect thread already has * it * => Deadlock: 4 threads are locked. * So to prevent that in such situation, the listen threads here * will both timeout trying to acquire the lock. The random time * for the timeout should allow on connection attempt to be * aborted whereas the other one should have time to finish in the * same time. * Warning: the minimum time (3s) should be big enough to allow * normal situation connections to terminate. The added random * time should represent a big enough range so that the chance to * have one listen thread timing out a lot before the peer one is * great. When the first listen thread times out, the remote * connect thread should release the lock and allow the peer * listen thread to take the lock it was waiting for and process * the connection attempt. */ Random random = new Random(); int randomTime = random.nextInt(6); // Random from 0 to 5 // Wait at least 3 seconds + (0 to 5 seconds) long timeout = (long) (3000 + ( randomTime * 1000 ) ); boolean noTimeout = replicationServerDomain.tryLock(timeout); if (!noTimeout) { // Timeout Message message = NOTE_TIMEOUT_WHEN_CROSS_CONNECTION.get( this.baseDn, Short.toString(serverId), Short.toString(replicationServer.getServerId())); closeSession(message); return; } } catch (InterruptedException ex) { // Thread interrupted, return. return; } } localGenerationId = replicationServerDomain.getGenerationId(); ServerState domServerState = replicationServerDomain.getDbServerState(); // The session initiator decides whether to use SSL. sslEncryption = inReplServerStartMsg.getSSLEncryption(); // Publish our start message outReplServerStartMsg = new ReplServerStartMsg(replicationServerId, replicationServerURL, this.baseDn, windowSize, domServerState, protocolVersion, localGenerationId, sslEncryption, replicationServer.getGroupId(), replicationServerDomain. getReplicationServer().getDegradedStatusThreshold()); if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { session.publish(outReplServerStartMsg); } else { // We support connection from a V1 RS, send PDU with V1 form session.publish(outReplServerStartMsg, ProtocolVersion.REPLICATION_PROTOCOL_V1); } if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ":" + "\nSH HANDSHAKE RECEIVED:\n" + inReplServerStartMsg.toString() + "\nAND REPLIED:\n" + outReplServerStartMsg.toString()); } } else { // Did the remote RS answer with the DN we provided him ? if (!(this.baseDn.equals(baseDn))) { Message message = ERR_RS_DN_DOES_NOT_MATCH.get( this.baseDn.toString(), baseDn.toString()); closeSession(message); if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); return; } if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ":" + "\nSH HANDSHAKE SENT:\n" + outReplServerStartMsg.toString() + "\nAND RECEIVED:\n" + inReplServerStartMsg.toString()); } } this.serverState = inReplServerStartMsg.getServerState(); sendWindowSize = inReplServerStartMsg.getWindowSize(); // Duplicate server ? if (!replicationServerDomain.checkForDuplicateRS(this)) { closeSession(null); if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); return; } /* Until here session is encrypted then it depends on the negociation */ if (!sslEncryption) { session.stopEncryption(); } } 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 closeSession(null); if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); return; } if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { // Only protocol version above V1 has a phase 2 handshake /* * NOW PROCEDE WITH SECOND PHASE OF HANDSHAKE: * TopologyMsg then TopologyMsg (with a RS) * OR * StartSessionMsg then TopologyMsg (with a DS) */ TopologyMsg outTopoMsg = null; if (baseDn != null) // Outgoing connection to a RS { // Send our own TopologyMsg to remote RS outTopoMsg = replicationServerDomain.createTopologyMsgForRS(); session.publish(outTopoMsg); } // Wait and process TopologyMsg or StartSessionMsg log_error_message = false; ReplicationMsg msg2 = session.receive(); log_error_message = true; if (msg2 instanceof TopologyMsg) { // Remote RS sent his topo msg TopologyMsg inTopoMsg = (TopologyMsg) msg2; // CONNECTION WITH A RS // if the remote RS and the local RS have the same genID // then it's ok and nothing else to do if (generationId == localGenerationId) { if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " RS with serverID=" + serverId + " is connected with the right generation ID"); } } else { if (localGenerationId > 0) { // if the local RS is initialized if (generationId > 0) { // if the remote RS is initialized if (generationId != localGenerationId) { // if the 2 RS have different generationID if (replicationServerDomain.getGenerationIdSavedStatus()) { // if the present RS has received changes regarding its // gen ID and so won't change without a reset // then we are just degrading the peer. Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( this.baseDn, Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } else { // The present RS has never received changes regarding its // gen ID. // // Example case: // - we are in RS1 // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2) // - RS1 has genId1 from LS1 /genId1 comes from data in // suffix // - we are in RS1 and we receive a START msg from RS2 // - Each RS keeps its genID / is degraded and when LS2 // will be populated from LS1 everything will become ok. // // Issue: // FIXME : Would it be a good idea in some cases to just // set the gen ID received from the peer RS // specially if the peer has a non null state and // we have a nul state ? // replicationServerDomain. // setGenerationId(generationId, false); Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( this.baseDn, Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } } } else { // The remote RS has no genId. We don't change anything for the // current RS. } } 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.setGenerationId(generationId, false); } } if (baseDn == null) // Reply to the RS (incoming connection) { // Send our own TopologyMsg to remote RS outTopoMsg = replicationServerDomain.createTopologyMsgForRS(); session.publish(outTopoMsg); if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ":" + "\nSH HANDSHAKE RECEIVED:\n" + inTopoMsg.toString() + "\nAND REPLIED:\n" + outTopoMsg.toString()); } } else { if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ":" + "\nSH HANDSHAKE SENT:\n" + outTopoMsg.toString() + "\nAND RECEIVED:\n" + inTopoMsg.toString()); } } // Alright, connected with new RS (either outgoing or incoming // connection): store handler. Map<Short, ServerHandler> connectedRSs = replicationServerDomain.getConnectedRSs(); connectedRSs.put(serverId, this); // Process TopologyMsg sent by remote RS: store matching new info // (this will also warn our connected DSs of the new received info) replicationServerDomain.receiveTopoInfoFromRS(inTopoMsg, this, false); } else if (msg2 instanceof StartSessionMsg) { // CONNECTION WITH A DS // Process StartSessionMsg sent by remote DS StartSessionMsg startSessionMsg = (StartSessionMsg) msg2; this.status = startSessionMsg.getStatus(); // Sanity check: is it a valid initial status? if (!isValidInitialStatus(this.status)) { Message mesg = ERR_RS_INVALID_INIT_STATUS.get( this.status.toString(), this.baseDn.toString(), Short.toString(serverId)); closeSession(mesg); if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); return; } this.refUrls = startSessionMsg.getReferralsURLs(); this.assuredFlag = startSessionMsg.isAssured(); this.assuredMode = startSessionMsg.getAssuredMode(); this.safeDataLevel = startSessionMsg.getSafeDataLevel(); /* * 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) */ if (localGenerationId > 0) { if (generationId != localGenerationId) { Message message = NOTE_BAD_GENERATION_ID_FROM_DS.get( this.baseDn, Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } } else { // We are an empty Replicationserver if ((generationId > 0) && (!serverState.isEmpty())) { // If the LDAP server has already sent changes // it is not expected to connect to an empty RS Message message = NOTE_BAD_GENERATION_ID_FROM_DS.get( this.baseDn, Short.toString(serverId), Long.toString(generationId), Long.toString(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.setGenerationId(generationId, false); } } // Send our own TopologyMsg to DS outTopoMsg = replicationServerDomain.createTopologyMsgForDS( this.serverId); session.publish(outTopoMsg); if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ":" + "\nSH HANDSHAKE RECEIVED:\n" + startSessionMsg.toString() + "\nAND REPLIED:\n" + outTopoMsg.toString()); } // Alright, connected with new DS: store handler. Map<Short, ServerHandler> connectedDSs = replicationServerDomain.getConnectedDSs(); connectedDSs.put(serverId, this); // Tell peer DSs a new DS just connected to us // No need to resend topo msg to this just new DS so not null // argument replicationServerDomain.sendTopoInfoToDSs(this); // Tell peer RSs a new DS just connected to us replicationServerDomain.sendTopoInfoToRSs(); } 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 closeSession(null); if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); return; } } else { // Terminate connection from a V1 RS // if the remote RS and the local RS have the same genID // then it's ok and nothing else to do if (generationId == localGenerationId) { if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " RS V1 with serverID=" + serverId + " is connected with the right generation ID"); } } else { if (localGenerationId > 0) { // if the local RS is initialized if (generationId > 0) { // if the remote RS is initialized if (generationId != localGenerationId) { // if the 2 RS have different generationID if (replicationServerDomain.getGenerationIdSavedStatus()) { // if the present RS has received changes regarding its // gen ID and so won't change without a reset // then we are just degrading the peer. Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( this.baseDn, Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } else { // The present RS has never received changes regarding its // gen ID. // // Example case: // - we are in RS1 // - RS2 has genId2 from LS2 (genId2 <=> no data in LS2) // - RS1 has genId1 from LS1 /genId1 comes from data in // suffix // - we are in RS1 and we receive a START msg from RS2 // - Each RS keeps its genID / is degraded and when LS2 // will be populated from LS1 everything will become ok. // // Issue: // FIXME : Would it be a good idea in some cases to just // set the gen ID received from the peer RS // specially if the peer has a non null state and // we have a nul state ? // replicationServerDomain. // setGenerationId(generationId, false); Message message = NOTE_BAD_GENERATION_ID_FROM_RS.get( this.baseDn, Short.toString(serverId), Long.toString(generationId), Long.toString(localGenerationId)); logError(message); } } } else { // The remote RS has no genId. We don't change anything for the // current RS. } } else { // The local RS is not initialized - take the one received oldGenerationId = replicationServerDomain.setGenerationId(generationId, false); } } // Alright, connected with new incoming V1 RS: store handler. Map<Short, ServerHandler> connectedRSs = replicationServerDomain.getConnectedRSs(); connectedRSs.put(serverId, this); // Note: the supported scenario for V1->V2 upgrade is to upgrade 1 by 1 // all the servers of the topology. We prefer not not send a TopologyMsg // for giving partial/false information to the V2 servers as for // instance we don't have the connected DS of the V1 RS...When the V1 // RS will be upgraded in his turn, topo info will be sent and accurate. // That way, there is no risk to have false/incomplete information in // other servers. } /* * FINALIZE INITIALIZATION: * CREATE READER AND WRITER, HEARTBEAT SYSTEM AND UPDATE MONITORING * SYSTEM */ // Disable timeout for next communications session.setSoTimeout(0); // sendWindow MUST be created before starting the writer sendWindow = new Semaphore(sendWindowSize); writer = new ServerWriter(session, serverId, this, replicationServerDomain); this, replicationServerDomain); reader = new ServerReader(session, serverId, this, replicationServerDomain); this, replicationServerDomain); reader.start(); writer.start(); @@ -1056,440 +388,30 @@ if (heartbeatInterval > 0) { heartbeatThread = new HeartbeatThread( "Replication Heartbeat to DS " + serverURL + " " + serverId + " for " + this.baseDn + " in RS " + replicationServerId, session, heartbeatInterval / 3); "Replication Heartbeat to " + this + " in RS " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName(), session, heartbeatInterval / 3); heartbeatThread.start(); } // Create the status analyzer for the domain if not already started if (serverIsLDAPserver) { if (!replicationServerDomain.isRunningStatusAnalyzer()) { if (debugEnabled()) TRACER.debugInfo("In " + replicationServerDomain. getReplicationServer(). getMonitorInstanceName() + " SH for remote server " + this.getMonitorInstanceName() + " is starting status analyzer"); replicationServerDomain.startStatusAnalyzer(); } } DirectoryServer.deregisterMonitorProvider(getMonitorInstanceName()); DirectoryServer.registerMonitorProvider(this); } 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 if (debugEnabled()) TRACER.debugInfo("In " + replicationServer.getMonitorInstanceName() + ":" + e.getMessage()); closeSession(null); } catch (Exception e) { // We do not want polluting error log if error is due to normal session // aborted after handshake phase one from a DS that is searching for best // suitable RS. if ( log_error_message || (baseDn != null) ) { // some problem happened, reject the connection MessageBuilder mb = new MessageBuilder(); mb.append(ERR_REPLICATION_SERVER_CONNECTION_ERROR.get( this.getMonitorInstanceName())); mb.append(": " + stackTraceToSingleLineString(e)); closeSession(mb.toMessage()); } else { closeSession(null); } // If generation id of domain was changed, set it back to old value // We may have changed it as it was -1 and we received a value >0 from // peer server and the last topo message sent may have failed being // sent: in that case retrieve old value of generation id for // replication server domain if (oldGenerationId != -100) { replicationServerDomain.setGenerationId(oldGenerationId, false); } } // Release domain if ((replicationServerDomain != null) && replicationServerDomain.hasLock()) replicationServerDomain.release(); } /* * Close the session logging the passed error message * Log nothing if message is null. */ private void closeSession(Message msg) { if (msg != null) { logError(msg); } try { session.close(); } catch (IOException ee) { // ignore } DirectoryServer.deregisterMonitorProvider(getMonitorInstanceName()); DirectoryServer.registerMonitorProvider(this); } /** * get the Server Id. * Sends a message containing a generationId to a peer server. * The peer is expected to be a replication server. * * @return the ID of the server to which this object is linked */ public short getServerId() { return serverId; } /** * Retrieves the Address URL for this server handler. * @param msg The GenerationIdMessage message to be sent. * @throws IOException When it occurs while sending the message, * * @return The Address URL for this server handler, * in the form of an IP address and port separated by a colon. */ public String getServerAddressURL() public void forwardGenerationIdToRS(ResetGenerationIdMsg msg) throws IOException { return serverAddressURL; } /** * Retrieves the URL for this server handler. * * @return The URL for this server handler, in the form of an address and * port separated by a colon. */ public String getServerURL() { return serverURL; } /** * Increase the counter of updates sent to the server. */ public void incrementOutCount() { outCount++; } /** * Increase the counter of update received from the server. */ public void incrementInCount() { inCount++; } /** * Get the count of updates received from the server. * @return the count of update received from the server. */ public int getInCount() { return inCount; } /** * Get the count of updates sent to this server. * @return The count of update sent to this server. */ public int getOutCount() { return outCount; } /** * Get the number of updates received from the server in assured safe read * mode. * @return The number of updates received from the server in assured safe read * mode */ public int getAssuredSrReceivedUpdates() { return assuredSrReceivedUpdates; } /** * Get the number of updates received from the server in assured safe read * mode that timed out. * @return The number of updates received from the server in assured safe read * mode that timed out. */ public AtomicInteger getAssuredSrReceivedUpdatesTimeout() { return assuredSrReceivedUpdatesTimeout; } /** * Get the number of updates sent to the server in assured safe read mode. * @return The number of updates sent to the server in assured safe read mode */ public int getAssuredSrSentUpdates() { return assuredSrSentUpdates; } /** * Get the number of updates sent to the server in assured safe read mode that * timed out. * @return The number of updates sent to the server in assured safe read mode * that timed out. */ public AtomicInteger getAssuredSrSentUpdatesTimeout() { return assuredSrSentUpdatesTimeout; } /** * Get the number of updates received from the server in assured safe data * mode. * @return The number of updates received from the server in assured safe data * mode */ public int getAssuredSdReceivedUpdates() { return assuredSdReceivedUpdates; } /** * Get the number of updates received from the server in assured safe data * mode that timed out. * @return The number of updates received from the server in assured safe data * mode that timed out. */ public AtomicInteger getAssuredSdReceivedUpdatesTimeout() { return assuredSdReceivedUpdatesTimeout; } /** * Get the number of updates sent to the server in assured safe data mode. * @return The number of updates sent to the server in assured safe data mode */ public int getAssuredSdSentUpdates() { return assuredSdSentUpdates; } /** * Get the number of updates sent to the server in assured safe data mode that * timed out. * @return The number of updates sent to the server in assured safe data mode * that timed out. */ public AtomicInteger getAssuredSdSentUpdatesTimeout() { return assuredSdSentUpdatesTimeout; } /** * Increment the number of updates received from the server in assured safe * read mode. */ public void incrementAssuredSrReceivedUpdates() { assuredSrReceivedUpdates++; } /** * Increment the number of updates received from the server in assured safe * read mode that timed out. */ public void incrementAssuredSrReceivedUpdatesTimeout() { assuredSrReceivedUpdatesTimeout.incrementAndGet(); } /** * Increment the number of updates sent to the server in assured safe read * mode. */ public void incrementAssuredSrSentUpdates() { assuredSrSentUpdates++; } /** * Increment the number of updates sent to the server in assured safe read * mode that timed out. */ public void incrementAssuredSrSentUpdatesTimeout() { assuredSrSentUpdatesTimeout.incrementAndGet(); } /** * Increment the number of updates received from the server in assured safe * data mode. */ public void incrementAssuredSdReceivedUpdates() { assuredSdReceivedUpdates++; } /** * Increment the number of updates received from the server in assured safe * data mode that timed out. */ public void incrementAssuredSdReceivedUpdatesTimeout() { assuredSdReceivedUpdatesTimeout.incrementAndGet(); } /** * Increment the number of updates sent to the server in assured safe data * mode. */ public void incrementAssuredSdSentUpdates() { assuredSdSentUpdates++; } /** * Increment the number of updates sent to the server in assured safe data * mode that timed out. */ public void incrementAssuredSdSentUpdatesTimeout() { assuredSdSentUpdatesTimeout.incrementAndGet(); } /** * Check is this server is saturated (this server has already been * sent a bunch of updates and has not processed them so they are staying * in the message queue for this server an the size of the queue * for this server is above the configured limit. * * The limit can be defined in number of updates or with a maximum delay * * @param changeNumber The changenumber to use to make the delay calculations. * @param sourceHandler The ServerHandler which is sending the update. * @return true is saturated false if not saturated. */ public boolean isSaturated(ChangeNumber changeNumber, ServerHandler sourceHandler) { synchronized (msgQueue) { int size = msgQueue.count(); if ((maxReceiveQueue > 0) && (size >= maxReceiveQueue)) return true; if ((sourceHandler.maxSendQueue > 0) && (size >= sourceHandler.maxSendQueue)) return true; if (!msgQueue.isEmpty()) { UpdateMsg firstUpdate = msgQueue.first(); if (firstUpdate != null) { long timeDiff = changeNumber.getTimeSec() - firstUpdate.getChangeNumber().getTimeSec(); if ((maxReceiveDelay > 0) && (timeDiff >= maxReceiveDelay)) return true; if ((sourceHandler.maxSendDelay > 0) && (timeDiff >= sourceHandler.maxSendDelay)) return true; } } return false; } } /** * Check that the size of the Server Handler messages Queue has lowered * below the limit and therefore allowing the reception of messages * from other servers to restart. * @param source The ServerHandler which was sending the update. * can be null. * @return true if the processing can restart */ public boolean restartAfterSaturation(ServerHandler source) { synchronized (msgQueue) { int queueSize = msgQueue.count(); if ((maxReceiveQueue > 0) && (queueSize >= restartReceiveQueue)) return false; if ((source != null) && (source.maxSendQueue > 0) && (queueSize >= source.restartSendQueue)) return false; if (!msgQueue.isEmpty()) { UpdateMsg firstUpdate = msgQueue.first(); UpdateMsg lastUpdate = msgQueue.last(); if ((firstUpdate != null) && (lastUpdate != null)) { long timeDiff = lastUpdate.getChangeNumber().getTimeSec() - firstUpdate.getChangeNumber().getTimeSec(); if ((maxReceiveDelay > 0) && (timeDiff >= restartReceiveDelay)) return false; if ((source != null) && (source.maxSendDelay > 0) && (timeDiff >= source.restartSendDelay)) return false; } } } return true; } /** * Check if the server associated to this ServerHandler is a replication * server. * @return true if the server associated to this ServerHandler is a * replication server. */ public boolean isReplicationServer() { return (!serverIsLDAPserver); } /** * Get the number of message in the receive message queue. * @return Size of the receive message queue. */ public int getRcvMsgQueueSize() { synchronized (msgQueue) { /* * When the server is up to date or close to be up to date, * the number of updates to be sent is the size of the receive queue. */ if (isFollowing()) return msgQueue.count(); else { /** * When the server is not able to follow, the msgQueue * may become too large and therefore won't contain all the * changes. Some changes may only be stored in the backing DB * of the servers. * The total size of the receive queue is calculated by doing * the sum of the number of missing changes for every dbHandler. */ ServerState dbState = replicationServerDomain.getDbServerState(); return ServerState.diffChanges(dbState, serverState); } } session.publish(msg); } /** @@ -1536,478 +458,133 @@ } /** * Get the older update time for that server. * @return The older update time. * Get the number of updates received from the server in assured safe data * mode. * @return The number of updates received from the server in assured safe data * mode */ public long getOlderUpdateTime() public int getAssuredSdReceivedUpdates() { ChangeNumber olderUpdateCN = getOlderUpdateCN(); if (olderUpdateCN == null) return 0; return olderUpdateCN.getTime(); return assuredSdReceivedUpdates; } /** * Get the older Change Number for that server. * Returns null when the queue is empty. * @return The older change number. * Get the number of updates received from the server in assured safe data * mode that timed out. * @return The number of updates received from the server in assured safe data * mode that timed out. */ public ChangeNumber getOlderUpdateCN() public AtomicInteger getAssuredSdReceivedUpdatesTimeout() { ChangeNumber result = null; synchronized (msgQueue) { if (isFollowing()) { if (msgQueue.isEmpty()) { result = null; } else { UpdateMsg msg = msgQueue.first(); result = msg.getChangeNumber(); } } else { if (lateQueue.isEmpty()) { // isFollowing is false AND lateQueue is empty // We may be at the very moment when the writer has emptyed the // lateQueue when it sent the last update. The writer will fill again // the lateQueue when it will send the next update but we are not yet // there. So let's take the last change not sent directly from // the db. ReplicationIteratorComparator comparator = new ReplicationIteratorComparator(); SortedSet<ReplicationIterator> iteratorSortedSet = new TreeSet<ReplicationIterator>(comparator); try { // Build a list of candidates iterator (i.e. db i.e. server) for (short serverId : replicationServerDomain.getServers()) { // get the last already sent CN from that server ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId); // get an iterator in this server db from that last change ReplicationIterator iterator = replicationServerDomain.getChangelogIterator(serverId, lastCsn); // if that iterator has changes, then it is a candidate // it is added in the sorted list at a position given by its // current change (see ReplicationIteratorComparator). if ((iterator != null) && (iterator.getChange() != null)) { iteratorSortedSet.add(iterator); } } UpdateMsg msg = iteratorSortedSet.first().getChange(); result = msg.getChangeNumber(); } catch (Exception e) { result = null; } finally { for (ReplicationIterator iterator : iteratorSortedSet) { iterator.releaseCursor(); } } } else { UpdateMsg msg = lateQueue.first(); result = msg.getChangeNumber(); } } } return result; return assuredSdReceivedUpdatesTimeout; } /** * Check if the LDAP server can follow the speed of the other servers. * @return true when the server has all the not yet sent changes * in its queue. * Get the number of updates sent to the server in assured safe data mode. * @return The number of updates sent to the server in assured safe data mode */ public boolean isFollowing() public int getAssuredSdSentUpdates() { return following; return assuredSdSentUpdates; } /** * Set the following flag of this server. * @param following the value that should be set. * Get the number of updates sent to the server in assured safe data mode that * timed out. * @return The number of updates sent to the server in assured safe data mode * that timed out. */ public void setFollowing(boolean following) public AtomicInteger getAssuredSdSentUpdatesTimeout() { this.following = following; return assuredSdSentUpdatesTimeout; } /** * Add an update to the list of updates that must be sent to the server * managed by this ServerHandler. * Get the number of updates received from the server in assured safe read * mode. * @return The number of updates received from the server in assured safe read * mode */ public int getAssuredSrReceivedUpdates() { return assuredSrReceivedUpdates; } /** * Get the number of updates received from the server in assured safe read * mode that timed out. * @return The number of updates received from the server in assured safe read * mode that timed out. */ public AtomicInteger getAssuredSrReceivedUpdatesTimeout() { return assuredSrReceivedUpdatesTimeout; } /** * Get the number of updates sent to the server in assured safe read mode. * @return The number of updates sent to the server in assured safe read mode */ public int getAssuredSrSentUpdates() { return assuredSrSentUpdates; } /** * Get the number of updates sent to the server in assured safe read mode that * timed out. * @return The number of updates sent to the server in assured safe read mode * that timed out. */ public AtomicInteger getAssuredSrSentUpdatesTimeout() { return assuredSrSentUpdatesTimeout; } /** * Returns the Replication Server Domain to which belongs this server handler. * * @param update The update that must be added to the list of updates. * @param sourceHandler The server that sent the update. * @return The replication server domain. */ public void add(UpdateMsg update, ServerHandler sourceHandler) public ReplicationServerDomain getDomain() { synchronized (msgQueue) { /* * If queue was empty the writer thread was probably asleep * waiting for some changes, wake it up */ if (msgQueue.isEmpty()) msgQueue.notify(); msgQueue.add(update); /* TODO : size should be configurable * and larger than max-receive-queue-size */ while ((msgQueue.count() > maxQueueSize) || (msgQueue.bytesCount() > maxQueueBytesSize)) { setFollowing(false); msgQueue.removeFirst(); } } if (isSaturated(update.getChangeNumber(), sourceHandler)) { sourceHandler.setSaturated(true); } } private void setSaturated(boolean value) { flowControl = value; return this.replicationServerDomain; } /** * Select the next update that must be sent to the server managed by this * ServerHandler. * * @return the next update that must be sent to the server managed by this * ServerHandler. * Returns the value of generationId for that handler. * @return The value of the generationId. */ public UpdateMsg take() public long getGenerationId() { boolean interrupted = true; UpdateMsg msg = getnextMessage(); /* * When we remove a message from the queue we need to check if another * server is waiting in flow control because this queue was too long. * This check might cause a performance penalty an therefore it * is not done for every message removed but only every few messages. */ if (++saturationCount > 10) { saturationCount = 0; try { replicationServerDomain.checkAllSaturation(); } catch (IOException e) { } } boolean acquired = false; do { try { acquired = sendWindow.tryAcquire((long) 500, TimeUnit.MILLISECONDS); interrupted = false; } catch (InterruptedException e) { // loop until not interrupted } } while (((interrupted) || (!acquired)) && (!shutdownWriter)); if (msg != null) { incrementOutCount(); if (msg.isAssured()) { if (msg.getAssuredMode() == AssuredMode.SAFE_READ_MODE) { incrementAssuredSrSentUpdates(); } else { if (!isLDAPserver()) incrementAssuredSdSentUpdates(); } } } return msg; return generationId; } /** * Get the next update that must be sent to the server * from the message queue or from the database. * * @return The next update that must be sent to the server. * Gets the group id of the server represented by this object. * @return The group id of the server represented by this object. */ private UpdateMsg getnextMessage() public byte getGroupId() { UpdateMsg msg; while (activeWriter == true) { if (following == false) { /* this server is late with regard to some other masters * in the topology or just joined the topology. * In such cases, we can't keep all changes in the queue * without saturating the memory, we therefore use * a lateQueue that is filled with a few changes from the changelogDB * If this server is able to close the gap, it will start using again * the regular msgQueue later. */ if (lateQueue.isEmpty()) { /* * Start from the server State * Loop until the queue high mark or until no more changes * for each known LDAP master * get the next CSN after this last one : * - try to get next from the file * - if not found in the file * - try to get the next from the queue * select the smallest of changes * check if it is in the memory tree * yes : lock memory tree. * check all changes from the list, remove the ones that * are already sent * unlock memory tree * restart as usual * load this change on the delayList * */ ReplicationIteratorComparator comparator = new ReplicationIteratorComparator(); SortedSet<ReplicationIterator> iteratorSortedSet = new TreeSet<ReplicationIterator>(comparator); /* fill the lateQueue */ for (short serverId : replicationServerDomain.getServers()) { ChangeNumber lastCsn = serverState.getMaxChangeNumber(serverId); ReplicationIterator iterator = replicationServerDomain.getChangelogIterator(serverId, lastCsn); if (iterator != null) { if (iterator.getChange() != null) { iteratorSortedSet.add(iterator); } else { iterator.releaseCursor(); } } } // The loop below relies on the fact that it is sorted based // on the currentChange of each iterator to consider the next // change across all servers. // Hence it is necessary to remove and eventual add again an iterator // when looping in order to keep consistent the order of the // iterators (see ReplicationIteratorComparator. while (!iteratorSortedSet.isEmpty() && (lateQueue.count()<100) && (lateQueue.bytesCount()<50000) ) { ReplicationIterator iterator = iteratorSortedSet.first(); iteratorSortedSet.remove(iterator); lateQueue.add(iterator.getChange()); if (iterator.next()) iteratorSortedSet.add(iterator); else iterator.releaseCursor(); } for (ReplicationIterator iterator : iteratorSortedSet) { iterator.releaseCursor(); } /* * Check if the first change in the lateQueue is also on the regular * queue */ if (lateQueue.isEmpty()) { synchronized (msgQueue) { if ((msgQueue.count() < maxQueueSize) && (msgQueue.bytesCount() < maxQueueBytesSize)) { setFollowing(true); } } } else { msg = lateQueue.first(); synchronized (msgQueue) { if (msgQueue.contains(msg)) { /* we finally catch up with the regular queue */ setFollowing(true); lateQueue.clear(); UpdateMsg msg1; do { msg1 = msgQueue.removeFirst(); } while (!msg.getChangeNumber().equals(msg1.getChangeNumber())); this.updateServerState(msg); return msg; } } } } else { /* get the next change from the lateQueue */ msg = lateQueue.removeFirst(); this.updateServerState(msg); return msg; } } synchronized (msgQueue) { if (following == true) { try { while (msgQueue.isEmpty() && (following == true)) { msgQueue.wait(500); if (!activeWriter) return null; } } catch (InterruptedException e) { return null; } if (following == true) { msg = msgQueue.removeFirst(); if (this.updateServerState(msg)) { /* * Only push the message if it has not yet been seen * by the other server. * Otherwise just loop to select the next message. */ return msg; } } } } /* * Need to loop because following flag may have gone to false between * the first check at the beginning of this method * and the second check just above. */ } return null; return groupId; } /** * Update the serverState with the last message sent. * * @param msg the last update sent. * @return boolean indicating if the update was meaningful. * Get our heartbeat interval. * @return Our heartbeat interval. */ public boolean updateServerState(UpdateMsg msg) public long getHeartbeatInterval() { return serverState.update(msg.getChangeNumber()); return heartbeatInterval; } /** * Get the state of this server. * * @return ServerState the state for this server.. * Get the count of updates received from the server. * @return the count of update received from the server. */ public ServerState getServerState() public int getInCount() { return serverState; } /** * Sends an ack message to the server represented by this object. * * @param ack The ack message to be sent. * @throws IOException In case of Exception thrown sending the ack. */ public void sendAck(AckMsg ack) throws IOException { session.publish(ack); } /** * Check type of server handled. * * @return true if the handled server is an LDAP server. * false if the handled server is a replicationServer */ public boolean isLDAPserver() { return serverIsLDAPserver; } /** * {@inheritDoc} */ @Override public void initializeMonitorProvider(MonitorProviderCfg configuration) throws ConfigException, InitializationException { // Nothing to do for now } /** * Retrieves the name of this monitor provider. It should be unique among all * monitor providers, including all instances of the same monitor provider. * * @return The name of this monitor provider. */ @Override public String getMonitorInstanceName() { String str = serverURL + " " + String.valueOf(serverId); if (serverIsLDAPserver) return "Connected Replica " + str + ",cn=" + replicationServerDomain.getMonitorInstanceName(); else return "Connected Replication Server " + str + ",cn=" + replicationServerDomain.getMonitorInstanceName(); } /** * Retrieves the length of time in milliseconds that should elapse between * calls to the <CODE>updateMonitorData()</CODE> method. A negative or zero * return value indicates that the <CODE>updateMonitorData()</CODE> method * should not be periodically invoked. * * @return The length of time in milliseconds that should elapse between * calls to the <CODE>updateMonitorData()</CODE> method. */ @Override public long getUpdateInterval() { /* we don't wont to do polling on this monitor */ return 0; } /** * Performs any processing periodic processing that may be desired to update * the information associated with this monitor. Note that best-effort * attempts will be made to ensure that calls to this method come * <CODE>getUpdateInterval()</CODE> milliseconds apart, but no guarantees will * be made. */ @Override public void updateMonitorData() { // As long as getUpdateInterval() returns 0, this will never get called return inCount; } /** @@ -2021,101 +598,11 @@ @Override public ArrayList<Attribute> getMonitorData() { ArrayList<Attribute> attributes = new ArrayList<Attribute>(); if (serverIsLDAPserver) { attributes.add(Attributes.create("replica", serverURL)); attributes.add(Attributes.create("connected-to", this.replicationServerDomain.getReplicationServer() .getMonitorInstanceName())); // Get the generic ones ArrayList<Attribute> attributes = super.getMonitorData(); } else { attributes.add(Attributes.create("Replication-Server", serverURL)); } attributes.add(Attributes.create("server-id", String .valueOf(serverId))); attributes.add(Attributes.create("domain-name", baseDn.toString())); try { MonitorData md; md = replicationServerDomain.computeMonitorData(); if (serverIsLDAPserver) { // 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()); } } else { // Missing changes long missingChanges = md.getMissingChangesRS(serverId); attributes.add(Attributes.create("missing-changes", String .valueOf(missingChanges))); /* get the Server State */ AttributeBuilder builder = new AttributeBuilder("server-state"); ServerState state = md.getRSStates(serverId); if (state != null) { for (String str : state.toStringSet()) { builder.add(str); } attributes.add(builder.toAttribute()); } } } catch (Exception e) { Message message = ERR_ERROR_RETRIEVING_MONITOR_DATA.get(stackTraceToSingleLineString(e)); // We failed retrieving the monitor data. attributes.add(Attributes.create("error", message.toString())); } attributes.add( Attributes.create("queue-size", String.valueOf(msgQueue.count()))); attributes.add( Attributes.create( "queue-size-bytes", String.valueOf(msgQueue.bytesCount()))); attributes.add( Attributes.create( "following", String.valueOf(following))); attributes.add(Attributes.create("server-id", String.valueOf(serverId))); attributes.add(Attributes.create("domain-name", getServiceId().toString())); // Deprecated attributes.add(Attributes.create("max-waiting-changes", String @@ -2129,23 +616,23 @@ attributes.add(Attributes.create("assured-sr-received-updates", String .valueOf(getAssuredSrReceivedUpdates()))); attributes.add(Attributes.create("assured-sr-received-updates-timeout", String .valueOf(getAssuredSrReceivedUpdatesTimeout()))); String .valueOf(getAssuredSrReceivedUpdatesTimeout()))); attributes.add(Attributes.create("assured-sr-sent-updates", String .valueOf(getAssuredSrSentUpdates()))); attributes.add(Attributes.create("assured-sr-sent-updates-timeout", String .valueOf(getAssuredSrSentUpdatesTimeout()))); attributes.add(Attributes.create("assured-sd-received-updates", String .valueOf(getAssuredSdReceivedUpdates()))); if (!isLDAPserver()) if (!isDataServer()) { attributes.add(Attributes.create("assured-sd-sent-updates", String.valueOf(getAssuredSdSentUpdates()))); String.valueOf(getAssuredSdSentUpdates()))); attributes.add(Attributes.create("assured-sd-sent-updates-timeout", String.valueOf(getAssuredSdSentUpdatesTimeout()))); String.valueOf(getAssuredSdSentUpdatesTimeout()))); } else { attributes.add(Attributes.create("assured-sd-received-updates-timeout", String.valueOf(getAssuredSdReceivedUpdatesTimeout()))); String.valueOf(getAssuredSdReceivedUpdatesTimeout()))); } // Window stats @@ -2170,44 +657,484 @@ } /** * Retrieves the name of this monitor provider. It should be unique among all * monitor providers, including all instances of the same monitor provider. * * @return The name of this monitor provider. */ @Override public abstract String getMonitorInstanceName(); /** * Get the older update time for that server. * @return The older update time. */ public long getOlderUpdateTime() { ChangeNumber olderUpdateCN = getOlderUpdateCN(); if (olderUpdateCN == null) return 0; return olderUpdateCN.getTime(); } /** * Get the count of updates sent to this server. * @return The count of update sent to this server. */ public int getOutCount() { return outCount; } /** * Gets the protocol version used with this remote server. * @return The protocol version used with this remote server. */ public short getProtocolVersion() { return protocolVersion; } /** * get the Server Id. * * @return the ID of the server to which this object is linked */ public short getServerId() { return serverId; } /** * Retrieves the URL for this server handler. * * @return The URL for this server handler, in the form of an address and * port separated by a colon. */ public String getServerURL() { return serverURL; } /** * Return the ServerStatus. * @return The server status. */ protected abstract ServerStatus getStatus(); /** * Retrieves the length of time in milliseconds that should elapse between * calls to the <CODE>updateMonitorData()</CODE> method. A negative or zero * return value indicates that the <CODE>updateMonitorData()</CODE> method * should not be periodically invoked. * * @return The length of time in milliseconds that should elapse between * calls to the <CODE>updateMonitorData()</CODE> method. */ @Override public long getUpdateInterval() { /* we don't wont to do polling on this monitor */ return 0; } /** * Increment the number of updates received from the server in assured safe * data mode. */ public void incrementAssuredSdReceivedUpdates() { assuredSdReceivedUpdates++; } /** * Increment the number of updates received from the server in assured safe * data mode that timed out. */ public void incrementAssuredSdReceivedUpdatesTimeout() { assuredSdReceivedUpdatesTimeout.incrementAndGet(); } /** * Increment the number of updates sent to the server in assured safe data * mode. */ public void incrementAssuredSdSentUpdates() { assuredSdSentUpdates++; } /** * Increment the number of updates sent to the server in assured safe data * mode that timed out. */ public void incrementAssuredSdSentUpdatesTimeout() { assuredSdSentUpdatesTimeout.incrementAndGet(); } /** * Increment the number of updates received from the server in assured safe * read mode. */ public void incrementAssuredSrReceivedUpdates() { assuredSrReceivedUpdates++; } /** * Increment the number of updates received from the server in assured safe * read mode that timed out. */ public void incrementAssuredSrReceivedUpdatesTimeout() { assuredSrReceivedUpdatesTimeout.incrementAndGet(); } /** * Increment the number of updates sent to the server in assured safe read * mode. */ public void incrementAssuredSrSentUpdates() { assuredSrSentUpdates++; } /** * Increment the number of updates sent to the server in assured safe read * mode that timed out. */ public void incrementAssuredSrSentUpdatesTimeout() { assuredSrSentUpdatesTimeout.incrementAndGet(); } /** * Increase the counter of update received from the server. */ public void incrementInCount() { inCount++; } /** * Increase the counter of updates sent to the server. */ public void incrementOutCount() { outCount++; } /** * {@inheritDoc} */ @Override public void initializeMonitorProvider(MonitorProviderCfg configuration) throws ConfigException, InitializationException { // Nothing to do for now } /** * Check if the server associated to this ServerHandler is a data server * in the topology. * @return true if the server is a data server. */ public abstract boolean isDataServer(); /** * Check if the server associated to this ServerHandler is a replication * server. * @return true if the server is a replication server. */ public boolean isReplicationServer() { return (!this.isDataServer()); } /** * Lock the domain potentially with a timeout. * @param timedout The provided timeout. * @throws DirectoryException When an exception occurs. */ protected void lockDomain(boolean timedout) throws DirectoryException { // The handshake phase must be done by blocking any access to structures // keeping info on connected servers, so that one can safely check for // pre-existence of a server, send a coherent snapshot of known topology // to peers, update the local view of the topology... // // For instance a kind of problem could be that while we connect with a // peer RS, a DS is connecting at the same time and we could publish the // connected DSs to the peer RS forgetting this last DS in the TopologyMsg. // // This method and every others that need to read/make changes to the // structures holding topology for the domain should: // - call ReplicationServerDomain.lock() // - read/modify structures // - call ReplicationServerDomain.release() // // More information is provided in comment of ReplicationServerDomain.lock() // If domain already exists, lock it until handshake is finished otherwise // it will be created and locked later in the method try { if (!timedout) { // !timedout if (!replicationServerDomain.hasLock()) replicationServerDomain.lock(); } else { // timedout /** * Take the lock on the domain. * WARNING: Here we try to acquire the lock with a timeout. This * is for preventing a deadlock that may happen if there are cross * connection attempts (for same domain) from this replication * server and from a peer one: * Here is the scenario: * - RS1 connect thread takes the domain lock and starts * connection to RS2 * - at the same time RS2 connect thread takes his domain lock and * start connection to RS2 * - RS2 listen thread starts processing received * ReplServerStartMsg from RS1 and wants to acquire the lock on * the domain (here) but cannot as RS2 connect thread already has * it * - RS1 listen thread starts processing received * ReplServerStartMsg from RS2 and wants to acquire the lock on * the domain (here) but cannot as RS1 connect thread already has * it * => Deadlock: 4 threads are locked. * So to prevent that in such situation, the listen threads here * will both timeout trying to acquire the lock. The random time * for the timeout should allow on connection attempt to be * aborted whereas the other one should have time to finish in the * same time. * Warning: the minimum time (3s) should be big enough to allow * normal situation connections to terminate. The added random * time should represent a big enough range so that the chance to * have one listen thread timing out a lot before the peer one is * great. When the first listen thread times out, the remote * connect thread should release the lock and allow the peer * listen thread to take the lock it was waiting for and process * the connection attempt. */ Random random = new Random(); int randomTime = random.nextInt(6); // Random from 0 to 5 // Wait at least 3 seconds + (0 to 5 seconds) long timeout = (long) (3000 + ( randomTime * 1000 ) ); boolean noTimeout = replicationServerDomain.tryLock(timeout); if (!noTimeout) { // Timeout Message message = NOTE_TIMEOUT_WHEN_CROSS_CONNECTION.get( getServiceId(), Short.toString(serverId), Short.toString(replicationServerId)); throw new DirectoryException(ResultCode.OTHER, message); } } } catch (InterruptedException e) { // Thread interrupted Message message = ERR_EXCEPTION_LOCKING_RS_DOMAIN.get(e.getMessage()); logError(message); } } /** * Processes a routable message. * * @param msg The message to be processed. */ public void process(RoutableMsg msg) { if (debugEnabled()) TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + this + " processes received msg:\n" + msg); replicationServerDomain.process(msg, this); } /** * Process the reception of a WindowProbeMsg message. * * @param windowProbeMsg The message to process. * * @throws IOException When the session becomes unavailable. */ public void process(WindowProbeMsg windowProbeMsg) throws IOException { if (rcvWindow > 0) { // The LDAP server believes that its window is closed // while it is not, this means that some problem happened in the // window exchange procedure ! // lets update the LDAP server with out current window size and hope // that everything will work better in the futur. // TODO also log an error message. WindowMsg msg = new WindowMsg(rcvWindow); session.publish(msg); } else { // Both the LDAP server and the replication server believes that the // window is closed. Lets check the flowcontrol in case we // can now resume operations and send a windowMessage if necessary. checkWindow(); } } /** * Send an InitializeRequestMessage to the server connected through this * handler. * * @param msg The message to be processed * @throws IOException when raised by the underlying session */ public void send(RoutableMsg msg) throws IOException { if (debugEnabled()) TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + this + " publishes message:\n" + msg); session.publish(msg); } /** * Sends an ack message to the server represented by this object. * * @param ack The ack message to be sent. * @throws IOException In case of Exception thrown sending the ack. */ public void sendAck(AckMsg ack) throws IOException { session.publish(ack); } /** * Send an ErrorMsg to the peer. * * @param errorMsg The message to be sent * @throws IOException when raised by the underlying session */ public void sendError(ErrorMsg errorMsg) throws IOException { session.publish(errorMsg); } /** * Send the ReplServerStartMsg to the remote server (RS or DS). * @param requestedProtocolVersion The provided protocol version. * @return The ReplServerStartMsg sent. * @throws IOException When an exception occurs. */ public ReplServerStartMsg sendStartToRemote(short requestedProtocolVersion) throws IOException { this.localGenerationId = replicationServerDomain.getGenerationId(); ReplServerStartMsg outReplServerStartMsg = new ReplServerStartMsg( replicationServerId, replicationServerURL, getServiceId(), maxRcvWindow, replicationServerDomain.getDbServerState(), protocolVersion, localGenerationId, sslEncryption, getLocalGroupId(), replicationServerDomain. getReplicationServer().getDegradedStatusThreshold()); if (requestedProtocolVersion>0) session.publish(outReplServerStartMsg, requestedProtocolVersion); else session.publish(outReplServerStartMsg); return outReplServerStartMsg; } /** * Sends the provided TopologyMsg to the peer server. * * @param topoMsg The TopologyMsg message to be sent. * @throws IOException When it occurs while sending the message, * */ public void sendTopoInfo(TopologyMsg topoMsg) throws IOException { // V1 Rs do not support the TopologyMsg if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { session.publish(topoMsg); } } /** * Set a new generation ID. * * @param generationId The new generation ID * */ public void setGenerationId(long generationId) { this.generationId = generationId; } /** * Sets the replication server domain associated. * @param rsd The provided replication server domain. */ protected void setReplicationServerDomain(ReplicationServerDomain rsd) { this.replicationServerDomain = rsd; } /** * Sets the window size when used when sending to the remote. * @param size The provided window size. */ protected void setSendWindowSize(int size) { this.sendWindowSize = size; } /** * Requests to shutdown the writer. */ protected void shutdownWriter() { shutdownWriter = true; } /** * Shutdown This ServerHandler. */ public void shutdown() { /* * Shutdown ServerWriter */ shutdownWriter = true; activeWriter = false; synchronized (msgQueue) { /* wake up the writer thread on an empty queue so that it disappear */ msgQueue.clear(); msgQueue.notify(); msgQueue.notifyAll(); } shutdownWriter(); setConsumerActive(false); super.shutdown(); /* * Close session to end ServerReader or ServerWriter */ try if (session != null) { session.close(); } catch (IOException e) { // ignore. } /* * Stop the remote LSHandler */ synchronized (directoryServers) { for (LightweightServerHandler lsh : directoryServers.values()) // Close session to end ServerReader or ServerWriter try { lsh.stopHandler(); session.close(); } catch (IOException e) { // ignore. } directoryServers.clear(); } /* @@ -2240,68 +1167,93 @@ { // don't try anymore to join and return. } if (debugEnabled()) TRACER.debugInfo("SH.shutdowned(" + this + ")"); } /** * {@inheritDoc} */ @Override public String toString() { String localString; if (serverId != 0) { if (serverIsLDAPserver) localString = "Directory Server "; else localString = "Replication Server "; localString += serverId + " " + serverURL + " " + baseDn; } else localString = "Unknown server"; return localString; } /** * Decrement the protocol window, then check if it is necessary * to send a WindowMsg and send it. * Select the next update that must be sent to the server managed by this * ServerHandler. * * @throws IOException when the session becomes unavailable. * @return the next update that must be sent to the server managed by this * ServerHandler. */ public synchronized void decAndCheckWindow() throws IOException public UpdateMsg take() { rcvWindow--; checkWindow(); } boolean interrupted = true; UpdateMsg msg = getnextMessage(true); // synchronous:block until msg /** * Check the protocol window and send WindowMsg if necessary. * * @throws IOException when the session becomes unavailable. */ public synchronized void checkWindow() throws IOException { if (rcvWindow < rcvWindowSizeHalf) /* * When we remove a message from the queue we need to check if another * server is waiting in flow control because this queue was too long. * This check might cause a performance penalty an therefore it * is not done for every message removed but only every few messages. */ if (++saturationCount > 10) { if (flowControl) saturationCount = 0; try { if (replicationServerDomain.restartAfterSaturation(this)) { flowControl = false; } } if (!flowControl) replicationServerDomain.checkAllSaturation(); } catch (IOException e) { WindowMsg msg = new WindowMsg(rcvWindowSizeHalf); session.publish(msg); rcvWindow += rcvWindowSizeHalf; } } boolean acquired = false; do { try { acquired = sendWindow.tryAcquire((long) 500, TimeUnit.MILLISECONDS); interrupted = false; } catch (InterruptedException e) { // loop until not interrupted } } while (((interrupted) || (!acquired)) && (!shutdownWriter)); if (msg != null) { incrementOutCount(); if (msg.isAssured()) { if (msg.getAssuredMode() == AssuredMode.SAFE_READ_MODE) { incrementAssuredSrSentUpdates(); } else { if (!isDataServer()) incrementAssuredSdSentUpdates(); } } } return msg; } /** * Creates a RSInfo structure representing this remote RS. * @return The RSInfo structure representing this remote RS */ public RSInfo toRSInfo() { RSInfo rsInfo = new RSInfo(serverId, generationId, groupId); return rsInfo; } /** * Performs any processing periodic processing that may be desired to update * the information associated with this monitor. Note that best-effort * attempts will be made to ensure that calls to this method come * <CODE>getUpdateInterval()</CODE> milliseconds apart, but no guarantees will * be made. */ @Override public void updateMonitorData() { // As long as getUpdateInterval() returns 0, this will never get called } /** * Update the send window size based on the credit specified in the * given window message. * @@ -2314,510 +1266,122 @@ } /** * Get our heartbeat interval. * @return Our heartbeat interval. * Log the messages involved in the start handshake. * @param inStartMsg The message received first. * @param outStartMsg The message sent in response. */ public long getHeartbeatInterval() { return heartbeatInterval; } /** * Processes a routable message. * * @param msg The message to be processed. */ public void process(RoutableMsg msg) protected void logStartHandshakeRCVandSND( StartMsg inStartMsg, StartMsg outStartMsg) { if (debugEnabled()) TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " SH for remote server " + this.getMonitorInstanceName() + ":" + "\nprocesses received msg:\n" + msg); replicationServerDomain.process(msg, this); } /** * Sends the provided TopologyMsg to the peer server. * * @param topoMsg The TopologyMsg message to be sent. * @throws IOException When it occurs while sending the message, * */ public void sendTopoInfo(TopologyMsg topoMsg) throws IOException { // V1 Rs do not support the TopologyMsg if (protocolVersion > ProtocolVersion.REPLICATION_PROTOCOL_V1) { if (debugEnabled()) TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " SH for remote server " + this.getMonitorInstanceName() + ":" + "\nsends message:\n" + topoMsg); session.publish(topoMsg); } } /** * Stores topology information received from a peer RS and that must be kept * in RS handler. * * @param topoMsg The received topology message */ public void receiveTopoInfoFromRS(TopologyMsg topoMsg) { // Store info for remote RS List<RSInfo> rsInfos = topoMsg.getRsList(); // List should only contain RS info for sender RSInfo rsInfo = rsInfos.get(0); generationId = rsInfo.getGenerationId(); groupId = rsInfo.getGroupId(); /** * Store info for DSs connected to the peer RS */ List<DSInfo> dsInfos = topoMsg.getDsList(); synchronized (directoryServers) { // Removes the existing structures for (LightweightServerHandler lsh : directoryServers.values()) { lsh.stopHandler(); } directoryServers.clear(); // Creates the new structure according to the message received. for (DSInfo dsInfo : dsInfos) { LightweightServerHandler lsh = new LightweightServerHandler(this, serverId, dsInfo.getDsId(), dsInfo.getGenerationId(), dsInfo.getGroupId(), dsInfo.getStatus(), dsInfo.getRefUrls(), dsInfo.isAssured(), dsInfo.getAssuredMode(), dsInfo.getSafeDataLevel()); lsh.startHandler(); directoryServers.put(lsh.getServerId(), lsh); } } } /** * 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) { // Sanity check if (!serverIsLDAPserver) { Message msg = ERR_RECEIVED_CHANGE_STATUS_NOT_FROM_DS.get(baseDn.toString(), Short.toString(serverId), csMsg.toString()); logError(msg); return ServerStatus.INVALID_STATUS; } // 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(), baseDn.toString(), Short.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(baseDn.toString(), Short.toString(serverId), status.toString(), event.toString()); logError(msg); return ServerStatus.INVALID_STATUS; } status = newStatus; return status; } /** * 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(baseDn.toString(), Short.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 " + baseDn + ":\n" + csMsg); } session.publish(csMsg); status = newStatus; return newStatus; } /** * When this handler is connected to a replication server, specifies if * a wanted server is connected to this replication server. * * @param wantedServer The server we want to know if it is connected * to the replication server represented by this handler. * @return boolean True is the wanted server is connected to the server * represented by this handler. */ public boolean isRemoteLDAPServer(short wantedServer) { synchronized (directoryServers) { for (LightweightServerHandler server : directoryServers.values()) { if (wantedServer == server.getServerId()) { return true; } } return false; } } /** * When the handler is connected to a replication server, specifies the * replication server has remote LDAP servers connected to it. * * @return boolean True is the replication server has remote LDAP servers * connected to it. */ public boolean hasRemoteLDAPServers() { synchronized (directoryServers) { return !directoryServers.isEmpty(); } } /** * Send an InitializeRequestMessage to the server connected through this * handler. * * @param msg The message to be processed * @throws IOException when raised by the underlying session */ public void send(RoutableMsg msg) throws IOException { if (debugEnabled()) TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " SH for remote server " + this.getMonitorInstanceName() + ":" + "\nsends message:\n" + msg); session.publish(msg); } /** * Send an ErrorMsg to the peer. * * @param errorMsg The message to be sent * @throws IOException when raised by the underlying session */ public void sendError(ErrorMsg errorMsg) throws IOException { session.publish(errorMsg); } /** * Process the reception of a WindowProbeMsg message. * * @param windowProbeMsg The message to process. * * @throws IOException When the session becomes unavailable. */ public void process(WindowProbeMsg windowProbeMsg) throws IOException { if (rcvWindow > 0) { // The LDAP server believes that its window is closed // while it is not, this means that some problem happened in the // window exchange procedure ! // lets update the LDAP server with out current window size and hope // that everything will work better in the futur. // TODO also log an error message. WindowMsg msg = new WindowMsg(rcvWindow); session.publish(msg); } else { // Both the LDAP server and the replication server believes that the // window is closed. Lets check the flowcontrol in case we // can now resume operations and send a windowMessage if necessary. checkWindow(); getMonitorInstanceName() + ", " + this.getClass().getSimpleName() + " " + this + ":" + "\nSH START HANDSHAKE RECEIVED:\n" + inStartMsg.toString()+ "\nAND REPLIED:\n" + outStartMsg.toString()); } } /** * Returns the value of generationId for that handler. * @return The value of the generationId. * Log the messages involved in the start handshake. * @param outStartMsg The message sent first. * @param inStartMsg The message received in response. */ public long getGenerationId() protected void logStartHandshakeSNDandRCV( StartMsg outStartMsg, StartMsg inStartMsg) { return generationId; } /** * Sends a message containing a generationId to a peer server. * The peer is expected to be a replication server. * * @param msg The GenerationIdMessage message to be sent. * @throws IOException When it occurs while sending the message, * */ public void forwardGenerationIdToRS(ResetGenerationIdMsg msg) throws IOException { session.publish(msg); } /** * Set a new generation ID. * * @param generationId The new generation ID * */ public void setGenerationId(long generationId) { this.generationId = generationId; } /** * Returns the Replication Server Domain to which belongs this server handler. * * @return The replication server domain. */ public ReplicationServerDomain getDomain() { return this.replicationServerDomain; } /** * Return a Set containing the servers known by this replicationServer. * @return a set containing the servers known by this replicationServer. */ public Set<Short> getConnectedDirectoryServerIds() { synchronized (directoryServers) { return directoryServers.keySet(); } } /** * 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 = null; 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 " + baseDn + " to force reconnection as new local" + " generation id 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. try { if (session != null) session.close(); } catch (IOException e) { // ignore } // 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 " + baseDn + " 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( Short.toString(replicationServerDomain. getReplicationServer().getServerId()), baseDn.toString(), Short.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(baseDn.toString(), Short.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 " + baseDn + ":\n" + csMsg); TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ", " + this.getClass().getSimpleName() + " " + this + ":" + "\nSH START HANDSHAKE SENT("+ this + "):\n" + outStartMsg.toString()+ "\nAND RECEIVED:\n" + inStartMsg.toString()); } session.publish(csMsg); status = newStatus; } /** * Set the shut down flag to true and returns the previous value of the flag. * @return The previous value of the shut down flag * Log the messages involved in the Topology handshake. * @param inTopoMsg The message received first. * @param outTopoMsg The message sent in response. */ public boolean engageShutdown() protected void logTopoHandshakeRCVandSND( TopologyMsg inTopoMsg, TopologyMsg outTopoMsg) { // Use thread safe boolean return shuttingDown.getAndSet(true); } /** * Gets the status of the connected DS. * @return The status of the connected DS. */ public ServerStatus getStatus() { return status; } /** * Gets the protocol version used with this remote server. * @return The protocol version used with this remote server. */ public short getProtocolVersion() { return protocolVersion; } /** * Add the DSinfos of the connected Directory Servers * to the List of DSInfo provided as a parameter. * * @param dsInfos The List of DSInfo that should be updated * with the DSInfo for the directoryServers * connected to this ServerHandler. */ public void addDSInfos(List<DSInfo> dsInfos) { synchronized (directoryServers) if (debugEnabled()) { for (LightweightServerHandler ls : directoryServers.values()) { dsInfos.add(ls.toDSInfo()); } TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ", " + this.getClass().getSimpleName() + " " + this + ":" + "\nSH TOPO HANDSHAKE RECEIVED:\n" + inTopoMsg.toString() + "\nAND REPLIED:\n" + outTopoMsg.toString()); } } /** * Gets the group id of the server represented by this object. * @return The group id of the server represented by this object. * Log the messages involved in the Topology handshake. * @param outTopoMsg The message sent first. * @param inTopoMsg The message received in response. */ public byte getGroupId() protected void logTopoHandshakeSNDandRCV( TopologyMsg outTopoMsg, TopologyMsg inTopoMsg) { return groupId; if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ", " + this.getClass().getSimpleName() + " " + this + ":" + "\nSH TOPO HANDSHAKE SENT:\n" + outTopoMsg.toString() + "\nAND RECEIVED:\n" + inTopoMsg.toString()); } } /** * Log the messages involved in the Topology/StartSession handshake. * @param inStartSessionMsg The message received first. * @param outTopoMsg The message sent in response. */ protected void logStartSessionHandshake( StartSessionMsg inStartSessionMsg, TopologyMsg outTopoMsg) { if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ", " + this.getClass().getSimpleName() + " " + this + " :" + "\nSH SESSION HANDSHAKE RECEIVED:\n" + "\nAND REPLIED:\n" + outTopoMsg.toString()); } } /** * Log the messages involved in the Topology/StartSession handshake. * @param inStartECLSessionMsg The message received first. */ protected void logStartECLSessionHandshake( StartECLSessionMsg inStartECLSessionMsg) { if (debugEnabled()) { TRACER.debugInfo("In " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + ", " + this.getClass().getSimpleName() + " " + this + " :" + "\nSH SESSION HANDSHAKE RECEIVED:\n" + inStartECLSessionMsg.toString()); } } } opends/src/server/org/opends/server/replication/server/ServerReader.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Copyright 2006-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.server; @@ -93,8 +93,9 @@ ServerHandler handler, ReplicationServerDomain replicationServerDomain) { super("Replication Reader for " + handler.toString() + " in RS " + replicationServerDomain.getReplicationServer().getServerId()); super("Replication Reader Thread for handler of " + handler.toString() + " in " + replicationServerDomain); this.session = session; this.serverId = serverId; this.handler = handler; @@ -106,13 +107,10 @@ */ public void run() { Message errMessage = null; if (debugEnabled()) { TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + (handler.isReplicationServer() ? " RS " : " LS") + " reader starting for serverId=" + serverId); TRACER.debugInfo(this.getName() + " starting"); } /* * wait on input stream @@ -127,16 +125,11 @@ { ReplicationMsg msg = session.receive(); /* if (debugEnabled()) { TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + (handler.isReplicationServer()?" From RS ":" From LS")+ " with serverId=" + serverId + " receives " + msg); TRACER.debugInfo(this.getName() + " receives " + msg); } */ if (msg instanceof AckMsg) { AckMsg ack = (AckMsg) msg; @@ -146,7 +139,7 @@ { boolean filtered = false; /* Ignore updates in some cases */ if (handler.isLDAPserver()) if (handler.isDataServer()) { /** * Ignore updates from DS in bad BAD_GENID_STATUS or @@ -250,12 +243,36 @@ } else if (msg instanceof TopologyMsg) { TopologyMsg topoMsg = (TopologyMsg) msg; replicationServerDomain.receiveTopoInfoFromRS(topoMsg, handler, true); try { ReplicationServerHandler rsh = (ReplicationServerHandler)handler; replicationServerDomain.receiveTopoInfoFromRS(topoMsg, rsh, true); } catch(Exception e) { errMessage = ERR_REPLICATION_PROTOCOL_MESSAGE_TYPE.get( "TopologyMsg", "other"); logError(errMessage); } } else if (msg instanceof ChangeStatusMsg) { ChangeStatusMsg csMsg = (ChangeStatusMsg) msg; replicationServerDomain.processNewStatus(handler, csMsg); try { DataServerHandler dsh = (DataServerHandler)handler; replicationServerDomain.processNewStatus(dsh, csMsg); } catch(Exception e) { errMessage = ERR_RECEIVED_CHANGE_STATUS_NOT_FROM_DS.get( replicationServerDomain.getBaseDn(), Short.toString(handler.getServerId()), csMsg.toString()); logError(errMessage); } } else if (msg instanceof MonitorRequestMsg) { MonitorRequestMsg replServerMonitorRequestMsg = @@ -271,8 +288,8 @@ * The remote server has sent an unknown message, * close the conenction. */ Message message = NOTE_READER_NULL_MSG.get(handler.toString()); logError(message); errMessage = NOTE_READER_NULL_MSG.get(handler.toString()); logError(errMessage); return; } } catch (NotSupportedOldVersionPDUException e) @@ -286,7 +303,8 @@ getMonitorInstanceName() + ":" + e.getMessage()); } } } catch (IOException e) } catch (IOException e) { /* * The connection has been broken @@ -297,13 +315,15 @@ TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " reader IO EXCEPTION for serverID=" + serverId + " reader IO EXCEPTION for serverID=" + serverId + " " + this + " " + stackTraceToSingleLineString(e) + " " + e.getLocalizedMessage()); Message message = NOTE_SERVER_DISCONNECT.get(handler.toString(), errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(), Short.toString(replicationServerDomain. getReplicationServer().getServerId())); logError(message); } catch (ClassNotFoundException e) logError(errMessage); } catch (ClassNotFoundException e) { if (debugEnabled()) TRACER.debugInfo( @@ -315,49 +335,48 @@ * The remote server has sent an unknown message, * close the connection. */ Message message = ERR_UNKNOWN_MESSAGE.get(handler.toString()); logError(message); } catch (Exception e) errMessage = ERR_UNKNOWN_MESSAGE.get(handler.toString()); logError(errMessage); } catch (Exception e) { if (debugEnabled()) TRACER.debugInfo( "In RS <" + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " server reader EXCEPTION serverID=" + serverId + stackTraceToSingleLineString(e)); " " + stackTraceToSingleLineString(e)); /* * The remote server has sent an unknown message, * close the connection. */ Message message = NOTE_READER_EXCEPTION.get(handler.toString()); logError(message); } finally errMessage = NOTE_READER_EXCEPTION.get(handler.toString()); logError(errMessage); } finally { /* * The thread only exit the loop above is some error condition * happen. * Attempt to close the socket and stop the server handler. */ if (debugEnabled()) TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + " server reader for serverID=" + serverId + " is closing the session"); try { if (debugEnabled()) TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + this + " is closing the session"); session.close(); } catch (IOException e) { // ignore } replicationServerDomain.stopServer(handler); if (debugEnabled()) { TRACER.debugInfo(this.getName() + " stopped " + errMessage); } } if (debugEnabled()) TRACER.debugInfo( "In RS " + replicationServerDomain.getReplicationServer(). getMonitorInstanceName() + (handler.isReplicationServer() ? " RS" : " LDAP") + " server reader stopped for serverID=" + serverId); } } opends/src/server/org/opends/server/replication/server/ServerWriter.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Copyright 2006-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.server; import org.opends.messages.Message; @@ -58,7 +58,6 @@ private ProtocolSession session; private ServerHandler handler; private ReplicationServerDomain replicationServerDomain; private short serverId; private short protocolVersion = -1; /** @@ -76,10 +75,10 @@ ServerHandler handler, ReplicationServerDomain replicationServerDomain) { super("Replication Writer for " + handler.toString() + " in RS " + replicationServerDomain.getReplicationServer().getServerId()); super("Replication Writer Thread for handler of " + handler.toString() + " in " + replicationServerDomain); this.serverId = serverId; this.session = session; this.handler = handler; this.replicationServerDomain = replicationServerDomain; @@ -94,16 +93,10 @@ */ public void run() { Message errMessage = null; if (debugEnabled()) { if (handler.isReplicationServer()) { TRACER.debugInfo("Replication server writer starting " + serverId); } else { TRACER.debugInfo("LDAP server writer starting " + serverId); } TRACER.debugInfo(this.getName() + " starting"); } try { @@ -111,10 +104,14 @@ { UpdateMsg update = replicationServerDomain.take(this.handler); if (update == null) { errMessage = Message.raw( "Connection closure: null update returned by domain."); return; /* this connection is closing */ } /* Ignore updates in some cases */ if (handler.isLDAPserver()) if (handler.isDataServer()) { /** * Ignore updates to DS in bad BAD_GENID_STATUS or FULL_UPDATE_STATUS @@ -184,6 +181,7 @@ ", writer to " + this.handler.getMonitorInstanceName() + " publishes msg=[" + update.toString() + "]"+ " refgenId=" + referenceGenerationId + " isAssured=" + update.isAssured() + " server=" + handler.getServerId() + " generationId=" + handler.getGenerationId()); } @@ -200,10 +198,10 @@ * The remote host has disconnected and this particular Tree is going to * be removed, just ignore the exception and let the thread die as well */ Message message = NOTE_SERVER_DISCONNECT.get(handler.toString(), errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(), Short.toString(replicationServerDomain. getReplicationServer().getServerId())); logError(message); logError(errMessage); } catch (SocketException e) { @@ -211,10 +209,10 @@ * The remote host has disconnected and this particular Tree is going to * be removed, just ignore the exception and let the thread die as well */ Message message = NOTE_SERVER_DISCONNECT.get(handler.toString(), errMessage = NOTE_SERVER_DISCONNECT.get(handler.toString(), Short.toString(replicationServerDomain. getReplicationServer().getServerId())); logError(message); logError(errMessage); } catch (Exception e) { @@ -222,9 +220,9 @@ * An unexpected error happened. * Log an error and close the connection. */ Message message = ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() + errMessage = ERR_WRITER_UNEXPECTED_EXCEPTION.get(handler.toString() + " " + stackTraceToSingleLineString(e)); logError(message); logError(errMessage); } finally { try @@ -235,17 +233,9 @@ // Can't do much more : ignore } replicationServerDomain.stopServer(handler); if (debugEnabled()) { if (handler.isReplicationServer()) { TRACER.debugInfo("Replication server writer stopping " + serverId); } else { TRACER.debugInfo("LDAP server writer stopping " + serverId); } TRACER.debugInfo(this.getName() + " stopped " + errMessage); } } } opends/src/server/org/opends/server/replication/server/StatusAnalyzer.java
@@ -22,14 +22,13 @@ * CDDL HEADER END * * * Copyright 2008 Sun Microsystems, Inc. * Copyright 2008-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.server; import static org.opends.server.loggers.debug.DebugLogger.debugEnabled; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import org.opends.server.api.DirectoryThread; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.common.ServerStatus; @@ -115,7 +114,7 @@ // Go through each connected DS, get the number of pending changes we have // for it and change status accordingly if threshold value is // crossed/uncrossed for (ServerHandler serverHandler : for (DataServerHandler serverHandler : replicationServerDomain.getConnectedDSs(). values()) { // Get number of pending changes for this server opends/src/server/org/opends/server/replication/service/ReplicationBroker.java
@@ -26,15 +26,9 @@ */ package org.opends.server.replication.service; import org.opends.server.replication.protocol.HeartbeatMonitor; import org.opends.messages.*; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.*; import org.opends.server.loggers.debug.DebugTracer; import static org.opends.messages.ReplicationMessages.*; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; import java.io.IOException; @@ -52,13 +46,33 @@ import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; import org.opends.messages.Category; import org.opends.messages.Message; import org.opends.messages.MessageBuilder; import org.opends.messages.Severity; import org.opends.server.api.DirectoryThread; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.DSInfo; import org.opends.server.replication.common.RSInfo; import org.opends.server.replication.common.ServerState; import org.opends.server.replication.common.ServerStatus; import org.opends.server.replication.protocol.*; import org.opends.server.replication.protocol.ChangeStatusMsg; import org.opends.server.replication.protocol.HeartbeatMonitor; import org.opends.server.replication.protocol.ProtocolSession; import org.opends.server.replication.protocol.ProtocolVersion; import org.opends.server.replication.protocol.ReplServerStartMsg; import org.opends.server.replication.protocol.ReplSessionSecurity; import org.opends.server.replication.protocol.ReplicationMsg; import org.opends.server.replication.protocol.ServerStartECLMsg; import org.opends.server.replication.protocol.ServerStartMsg; import org.opends.server.replication.protocol.StartECLSessionMsg; import org.opends.server.replication.protocol.StartSessionMsg; import org.opends.server.replication.protocol.TopologyMsg; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.protocol.WindowMsg; import org.opends.server.replication.protocol.WindowProbeMsg; import org.opends.server.util.ServerConstants; import org.opends.server.replication.server.ReplicationServer; /** @@ -300,6 +314,43 @@ } } private void connect() { if (this.baseDn.compareToIgnoreCase( ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT)==0) { connectAsECL(); } else { connectAsDataServer(); } } /** * Special aspects of connecting as ECL compared to connecting as data server * are : * - 1 single RS configured * - so no choice of the prefered RS * - No same groupID polling * - ?? Heartbeat * - Start handshake is : * Broker ---> StartECLMsg ---> RS * <---- ReplServerStartMsg --- * ---> StartSessionECLMsg --> RS */ private void connectAsECL() { // FIXME:ECL List of RS to connect is for now limited to one RS only String bestServer = this.servers.iterator().next(); ReplServerStartMsg inReplServerStartMsg = performECLPhaseOneHandshake(bestServer, true); if (inReplServerStartMsg!=null) performECLPhaseTwoHandshake(bestServer); } /** * Connect to a ReplicationServer. * @@ -325,7 +376,7 @@ * * @throws NumberFormatException address was invalid */ private void connect() private void connectAsDataServer() { HashMap<String, ServerInfo> rsInfos = new HashMap<String, ServerInfo>(); @@ -350,6 +401,9 @@ * Connect to each replication server and get their ServerState then find * out which one is the best to connect to. */ if (debugEnabled()) TRACER.debugInfo("phase 1 : will perform PhaseOneH with each RS in " + " order to elect the prefered one"); for (String server : servers) { // Connect to server and get reply message @@ -375,6 +429,9 @@ serverId, baseDn, groupId); // Best found, now initialize connection to this one (handshake phase 1) if (debugEnabled()) TRACER.debugInfo( "phase 2 : will perform PhaseOneH with the prefered RS."); replServerStartMsg = performPhaseOneHandshake(bestServer, true); if (replServerStartMsg != null) // Handshake phase 1 exchange went well @@ -764,6 +821,8 @@ { try { if (debugEnabled()) TRACER.debugInfo("In RB, closing session after phase 1"); localSession.close(); } catch (IOException e) { @@ -789,6 +848,225 @@ } /** * Connect to the provided server performing the first phase handshake * (start messages exchange) and return the reply message from the replication * server. * * @param server Server to connect to. * @param keepConnection Do we keep session opened or not after handshake. * Use true if want to perform handshake phase 2 with the same session * and keep the session to create as the current one. * @return The ReplServerStartMsg the server replied. Null if could not * get an answer. */ private ReplServerStartMsg performECLPhaseOneHandshake(String server, boolean keepConnection) { ReplServerStartMsg replServerStartMsg = null; // Parse server string. int separator = server.lastIndexOf(':'); String port = server.substring(separator + 1); String hostname = server.substring(0, separator); ProtocolSession localSession = null; boolean error = false; try { /* * Open a socket connection to the next candidate. */ int intPort = Integer.parseInt(port); InetSocketAddress serverAddr = new InetSocketAddress( InetAddress.getByName(hostname), intPort); if (keepConnection) tmpReadableServerName = serverAddr.toString(); Socket socket = new Socket(); socket.setReceiveBufferSize(1000000); socket.setTcpNoDelay(true); socket.connect(serverAddr, 500); localSession = replSessionSecurity.createClientSession(server, socket, ReplSessionSecurity.HANDSHAKE_TIMEOUT); boolean isSslEncryption = replSessionSecurity.isSslEncryption(server); // Send our start msg. ServerStartECLMsg serverStartECLMsg = new ServerStartECLMsg( baseDn, 0, 0, 0, 0, maxRcvWindow, heartbeatInterval, state, ProtocolVersion.getCurrentVersion(), this.getGenerationID(), isSslEncryption, groupId); localSession.publish(serverStartECLMsg); // Read the ReplServerStartMsg that should come back. replServerStartMsg = (ReplServerStartMsg) localSession.receive(); if (debugEnabled()) { TRACER.debugInfo("In RB for " + baseDn + "\nRB HANDSHAKE SENT:\n" + serverStartECLMsg.toString() + "\nAND RECEIVED:\n" + replServerStartMsg.toString()); } // Sanity check String repDn = replServerStartMsg.getBaseDn(); if (!(this.baseDn.equals(repDn))) { Message message = ERR_DS_DN_DOES_NOT_MATCH.get(repDn.toString(), this.baseDn); logError(message); error = true; } /* * We have sent our own protocol version to the replication server. * The replication server will use the same one (or an older one * if it is an old replication server). */ if (keepConnection) protocolVersion = ProtocolVersion.minWithCurrent( replServerStartMsg.getVersion()); if (!isSslEncryption) { localSession.stopEncryption(); } } catch (ConnectException e) { /* * There was no server waiting on this host:port * Log a notice and try the next replicationServer in the list */ if (!connectionError) { Message message = NOTE_NO_CHANGELOG_SERVER_LISTENING.get(server); if (keepConnection) // Log error message only for final connection { // the error message is only logged once to avoid overflowing // the error log logError(message); } else if (debugEnabled()) { TRACER.debugInfo(message.toString()); } } error = true; } catch (Exception e) { if ( (e instanceof SocketTimeoutException) && debugEnabled() ) { TRACER.debugInfo("Timeout trying to connect to RS " + server + " for dn: " + baseDn); } Message message = ERR_EXCEPTION_STARTING_SESSION_PHASE.get("1", baseDn, server, e.getLocalizedMessage() + stackTraceToSingleLineString(e)); if (keepConnection) // Log error message only for final connection { logError(message); } else if (debugEnabled()) { TRACER.debugInfo(message.toString()); } error = true; } // Close session if requested if (!keepConnection || error) { if (localSession != null) { try { if (debugEnabled()) TRACER.debugInfo("In RB, closing session after phase 1"); localSession.close(); } catch (IOException e) { // The session was already closed, just ignore. } localSession = null; } if (error) { replServerStartMsg = null; } // Be sure to return null. } // If this connection as the one to use for sending and receiving updates, // store it. if (keepConnection) { session = localSession; } return replServerStartMsg; } /** * Performs the second phase handshake (send StartSessionMsg and receive * TopologyMsg messages exchange) and return the reply message from the * replication server. * * @param server Server we are connecting with. * @param initStatus The status we are starting with * @return The ReplServerStartMsg the server replied. Null if could not * get an answer. */ private TopologyMsg performECLPhaseTwoHandshake(String server) { TopologyMsg topologyMsg = null; try { // Send our Start Session StartECLSessionMsg startECLSessionMsg = null; startECLSessionMsg = new StartECLSessionMsg(); startECLSessionMsg.setOperationId(Short.toString(serverId)); session.publish(startECLSessionMsg); /* FIXME:ECL In the handshake phase two, should RS send back a topo msg ? * Read the TopologyMsg that should come back. topologyMsg = (TopologyMsg) session.receive(); */ if (debugEnabled()) { TRACER.debugInfo("In RB for " + baseDn + "\nRB HANDSHAKE SENT:\n" + startECLSessionMsg.toString()); // + "\nAND RECEIVED:\n" + topologyMsg.toString()); } // Alright set the timeout to the desired value session.setSoTimeout(timeout); connected = true; } catch (Exception e) { Message message = ERR_EXCEPTION_STARTING_SESSION_PHASE.get("2", baseDn, server, e.getLocalizedMessage() + stackTraceToSingleLineString(e)); logError(message); if (session != null) { try { session.close(); } catch (IOException ex) { // The session was already closed, just ignore. } session = null; } // Be sure to return null. topologyMsg = null; } return topologyMsg; } /** * Performs the second phase handshake (send StartSessionMsg and receive * TopologyMsg messages exchange) and return the reply message from the * replication server. @@ -1720,6 +1998,12 @@ { boolean done = false; if (debugEnabled()) { TRACER.debugInfo("SameGroupIdPoller for: " + baseDn.toString() + " started."); } while ((!done) && (!sameGroupIdPollershutdown)) { // Sleep some time between checks @@ -1850,11 +2134,6 @@ */ public void receiveTopo(TopologyMsg topoMsg) { if (debugEnabled()) TRACER.debugInfo("Replication domain " + baseDn + " received topology info update:\n" + topoMsg); // Store new lists synchronized(getDsList()) { opends/src/server/org/opends/server/util/ServerConstants.java
@@ -747,6 +747,12 @@ public static final String OC_GROUP_OF_URLS_LC = "groupofurls"; /** * The name of the objectclass that will be used as the structural class for * monitor entries. */ public static final String OC_CHANGELOG_ENTRY = "changeLogEntry"; /** * The request OID for the cancel extended operation. @@ -1961,6 +1967,13 @@ "1.3.6.1.4.1.42.2.27.9.5.8"; /** * The OID for the entry change request control. * FIXME:ECL ask for OID_ECL_REQUEST_CONTROL */ public static final String OID_ECL_COOKIE_EXCHANGE_CONTROL = "1.3.6.1.4.1.26027.1.5.4"; /** * The IANA-assigned OID for the feature allowing a user to request that all @@ -2983,5 +2996,12 @@ * The normalized false value. */ public static final ByteString FALSE_VALUE = ByteString.valueOf("FALSE"); /** * The root Dn for the external change log. */ public static final String DN_EXTERNAL_CHANGELOG_ROOT = "cn=changelog"; } opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLSearchOperation.java
New file @@ -0,0 +1,1300 @@ /* * 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 2008-2009 Sun Microsystems, Inc. */ package org.opends.server.workflowelement.externalchangelog; import static org.opends.messages.CoreMessages.*; 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.*; import static org.opends.server.util.StaticUtils.getExceptionMessage; import static org.opends.server.util.StaticUtils.needsBase64Encoding; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.TimeZone; import org.opends.messages.Category; import org.opends.messages.Message; import org.opends.messages.Severity; import org.opends.server.api.ClientConnection; import org.opends.server.api.plugin.PluginResult; import org.opends.server.controls.EntryChangelogNotificationControl; import org.opends.server.controls.ExternalChangelogRequestControl; import org.opends.server.controls.LDAPAssertionRequestControl; import org.opends.server.controls.MatchedValuesControl; import org.opends.server.controls.PersistentSearchControl; import org.opends.server.controls.ProxiedAuthV1Control; import org.opends.server.controls.ProxiedAuthV2Control; import org.opends.server.core.AccessControlConfigManager; import org.opends.server.core.AddOperation; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyOperation; import org.opends.server.core.PersistentSearch; import org.opends.server.core.PluginConfigManager; import org.opends.server.core.SearchOperation; import org.opends.server.core.SearchOperationWrapper; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.ExternalChangeLogSession; import org.opends.server.replication.common.MultiDomainServerState; import org.opends.server.replication.plugin.MultimasterReplication; import org.opends.server.replication.protocol.AddMsg; import org.opends.server.replication.protocol.DeleteMsg; import org.opends.server.replication.protocol.ECLUpdateMsg; import org.opends.server.replication.protocol.ModifyDNMsg; import org.opends.server.replication.protocol.ModifyMsg; import org.opends.server.replication.protocol.StartECLSessionMsg; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.server.ReplicationServer; import org.opends.server.replication.service.ReplicationDomain; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.AttributeValue; import org.opends.server.types.Attributes; import org.opends.server.types.CancelRequest; import org.opends.server.types.CancelResult; import org.opends.server.types.CanceledOperationException; import org.opends.server.types.Control; import org.opends.server.types.DN; import org.opends.server.types.DebugLogLevel; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.Modification; import org.opends.server.types.ObjectClass; import org.opends.server.types.Privilege; import org.opends.server.types.RawAttribute; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchFilter; import org.opends.server.types.SearchScope; import org.opends.server.types.operation.PostOperationSearchOperation; import org.opends.server.types.operation.PreOperationSearchOperation; import org.opends.server.types.operation.SearchEntrySearchOperation; import org.opends.server.types.operation.SearchReferenceSearchOperation; import org.opends.server.util.Base64; import org.opends.server.util.ServerConstants; import org.opends.server.util.TimeThread; /** * This class defines an operation used to search for entries in a local backend * of the Directory Server. */ public class ECLSearchOperation extends SearchOperationWrapper implements PreOperationSearchOperation, PostOperationSearchOperation, SearchEntrySearchOperation, SearchReferenceSearchOperation { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** * The ECL Start Session we'll send to the RS. */ private StartECLSessionMsg startECLSessionMsg; // The set of objectclasses that will be used in ECL entries. private static HashMap<ObjectClass,String> eclObjectClasses; // The associated DN. private DN rootBaseDN; // The cookie received in the ECL request control coming along // with the request. MultiDomainServerState requestCookie = null; /** * The replication server in which the search on ECL is to be performed. */ protected ReplicationServer replicationServer; /** * Indicates whether we should actually process the search. This should * only be false if it's a persistent search with changesOnly=true. */ protected boolean changesOnly; /** * The client connection for the search operation. */ protected ClientConnection clientConnection; /** * The base DN for the search. */ protected DN baseDN; /** * The persistent search request, if applicable. */ protected PersistentSearch persistentSearch; /** * The filter for the search. */ protected SearchFilter filter; private ExternalChangeLogSession eclSession; // The set of supported controls for this WE private HashSet<String> supportedControls; // The set of supported features for this WE private HashSet<String> supportedFeatures; String privateDomainsBaseDN; /** * Creates a new operation that may be used to search for entries in a local * backend of the Directory Server. * * @param search The operation to process. */ public ECLSearchOperation(SearchOperation search) { super(search); try { rootBaseDN = DN.decode(ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT); } catch (Exception e){} // Construct the set of objectclasses to include in the base monitor entry. eclObjectClasses = new LinkedHashMap<ObjectClass,String>(2); ObjectClass topOC = DirectoryServer.getObjectClass(OC_TOP, true); eclObjectClasses.put(topOC, OC_TOP); ObjectClass eclEntryOC = DirectoryServer.getObjectClass(OC_CHANGELOG_ENTRY, true); eclObjectClasses.put(eclEntryOC, OC_CHANGELOG_ENTRY); // Define an empty sets for the supported controls and features. supportedControls = new HashSet<String>(0); supportedControls.add(ServerConstants.OID_SERVER_SIDE_SORT_REQUEST_CONTROL); supportedControls.add(ServerConstants.OID_VLV_REQUEST_CONTROL); supportedFeatures = new HashSet<String>(0); ECLWorkflowElement.attachLocalOperation(search, this); } /** * Process this search operation against a local backend. * * @param wfe * The local backend work-flow element. * @throws CanceledOperationException * if this operation should be cancelled */ public void processECLSearch(ECLWorkflowElement wfe) throws CanceledOperationException { boolean executePostOpPlugins = false; this.replicationServer = wfe.getReplicationServer(); clientConnection = getClientConnection(); // Get the plugin config manager that will be used for invoking plugins. PluginConfigManager pluginConfigManager = DirectoryServer.getPluginConfigManager(); changesOnly = false; // Check for a request to cancel this operation. checkIfCanceled(false); // Create a labeled block of code that we can break out of if a problem is // detected. searchProcessing: { // Process the search base and filter to convert them from their raw forms // as provided by the client to the forms required for the rest of the // search processing. baseDN = getBaseDN(); filter = getFilter(); if ((baseDN == null) || (filter == null)){ break searchProcessing; } // Check to see if there are any controls in the request. If so, then // see if there is any special processing required. try { this.requestCookie = null; handleRequestControls(); if (this.requestCookie == null) { setResponseData(new DirectoryException( ResultCode.OPERATIONS_ERROR, Message.raw(Category.SYNC, Severity.FATAL_ERROR, "Cookie control expected"))); break searchProcessing; } } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); break searchProcessing; } // Check for a request to cancel this operation. checkIfCanceled(false); // Invoke the pre-operation search plugins. executePostOpPlugins = true; PluginResult.PreOperation preOpResult = pluginConfigManager.invokePreOperationSearchPlugins(this); if (!preOpResult.continueProcessing()) { setResultCode(preOpResult.getResultCode()); appendErrorMessage(preOpResult.getErrorMessage()); setMatchedDN(preOpResult.getMatchedDN()); setReferralURLs(preOpResult.getReferralURLs()); break searchProcessing; } // Check for a request to cancel this operation. checkIfCanceled(false); // Test existence of the RS if (replicationServer == null) { setResultCode(ResultCode.OPERATIONS_ERROR); appendErrorMessage(ERR_SEARCH_BASE_DOESNT_EXIST.get( String.valueOf(baseDN))); break searchProcessing; } // We'll set the result code to "success". If a problem occurs, then it // will be overwritten. setResultCode(ResultCode.SUCCESS); // If there's a persistent search, then register it with the server. if (persistentSearch != null) { wfe.registerPersistentSearch(persistentSearch); persistentSearch.enable(); } // Process the search. try { processSearch(); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } setResponseData(de); if (persistentSearch != null) { persistentSearch.cancel(); setSendResponse(true); } break searchProcessing; } catch (CanceledOperationException coe) { if (persistentSearch != null) { persistentSearch.cancel(); setSendResponse(true); } throw coe; } catch (Exception e) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } setResultCode(DirectoryServer.getServerErrorResultCode()); appendErrorMessage(ERR_SEARCH_BACKEND_EXCEPTION.get( getExceptionMessage(e))); if (persistentSearch != null) { persistentSearch.cancel(); setSendResponse(true); } break searchProcessing; } } // Check for a request to cancel this operation. checkIfCanceled(false); // Invoke the post-operation search plugins. if (executePostOpPlugins) { PluginResult.PostOperation postOpResult = pluginConfigManager.invokePostOperationSearchPlugins(this); if (!postOpResult.continueProcessing()) { setResultCode(postOpResult.getResultCode()); appendErrorMessage(postOpResult.getErrorMessage()); setMatchedDN(postOpResult.getMatchedDN()); setReferralURLs(postOpResult.getReferralURLs()); } } } /** * Handles any controls contained in the request. * * @throws DirectoryException If there is a problem with any of the request * controls. */ protected void handleRequestControls() throws DirectoryException { List<Control> requestControls = getRequestControls(); if ((requestControls != null) && (! requestControls.isEmpty())) { for (int i=0; i < requestControls.size(); i++) { Control c = requestControls.get(i); String oid = c.getOID(); if (! AccessControlConfigManager.getInstance(). getAccessControlHandler().isAllowed(baseDN, this, c)) { throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); } if (oid.equals(OID_ECL_COOKIE_EXCHANGE_CONTROL)) { ExternalChangelogRequestControl eclControl = getRequestControl(ExternalChangelogRequestControl.DECODER); this.requestCookie = eclControl.getCookie(); } else if (oid.equals(OID_LDAP_ASSERTION)) { LDAPAssertionRequestControl assertControl = getRequestControl(LDAPAssertionRequestControl.DECODER); try { // FIXME -- We need to determine whether the current user has // permission to make this determination. SearchFilter assertionFilter = assertControl.getSearchFilter(); Entry entry; try { entry = DirectoryServer.getEntry(baseDN); } catch (DirectoryException de) { if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(de.getResultCode(), ERR_SEARCH_CANNOT_GET_ENTRY_FOR_ASSERTION.get( de.getMessageObject())); } if (entry == null) { throw new DirectoryException(ResultCode.NO_SUCH_OBJECT, ERR_SEARCH_NO_SUCH_ENTRY_FOR_ASSERTION.get()); } if (! assertionFilter.matchesEntry(entry)) { throw new DirectoryException(ResultCode.ASSERTION_FAILED, ERR_SEARCH_ASSERTION_FAILED.get()); } } catch (DirectoryException de) { if (de.getResultCode() == ResultCode.ASSERTION_FAILED) { throw de; } if (debugEnabled()) { TRACER.debugCaught(DebugLogLevel.ERROR, de); } throw new DirectoryException(ResultCode.PROTOCOL_ERROR, ERR_SEARCH_CANNOT_PROCESS_ASSERTION_FILTER.get( de.getMessageObject()), de); } } else if (oid.equals(OID_PROXIED_AUTH_V1)) { // The requester must have the PROXIED_AUTH privilige in order to be // able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV1Control proxyControl = getRequestControl(ProxiedAuthV1Control.DECODER); Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PROXIED_AUTH_V2)) { // The requester must have the PROXIED_AUTH privilige in order to be // able to use this control. if (! clientConnection.hasPrivilege(Privilege.PROXIED_AUTH, this)) { throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, ERR_PROXYAUTH_INSUFFICIENT_PRIVILEGES.get()); } ProxiedAuthV2Control proxyControl = getRequestControl(ProxiedAuthV2Control.DECODER); Entry authorizationEntry = proxyControl.getAuthorizationEntry(); setAuthorizationEntry(authorizationEntry); if (authorizationEntry == null) { setProxiedAuthorizationDN(DN.nullDN()); } else { setProxiedAuthorizationDN(authorizationEntry.getDN()); } } else if (oid.equals(OID_PERSISTENT_SEARCH)) { PersistentSearchControl psearchControl = getRequestControl(PersistentSearchControl.DECODER); persistentSearch = new PersistentSearch(this, psearchControl.getChangeTypes(), psearchControl.getReturnECs()); // If we're only interested in changes, then we don't actually want // to process the search now. if (psearchControl.getChangesOnly()) { changesOnly = true; } } else if (oid.equals(OID_LDAP_SUBENTRIES)) { setReturnLDAPSubentries(true); } else if (oid.equals(OID_MATCHED_VALUES)) { MatchedValuesControl matchedValuesControl = getRequestControl(MatchedValuesControl.DECODER); setMatchedValuesControl(matchedValuesControl); } else if (oid.equals(OID_ACCOUNT_USABLE_CONTROL)) { setIncludeUsableControl(true); } else if (oid.equals(OID_REAL_ATTRS_ONLY)) { setRealAttributesOnly(true); } else if (oid.equals(OID_VIRTUAL_ATTRS_ONLY)) { setVirtualAttributesOnly(true); } else if (oid.equals(OID_GET_EFFECTIVE_RIGHTS) && DirectoryServer.isSupportedControl(OID_GET_EFFECTIVE_RIGHTS)) { // Do nothing here and let AciHandler deal with it. } // NYI -- Add support for additional controls. else if (c.isCritical()) { if ((replicationServer == null) || (! supportsControl(oid))) { throw new DirectoryException( ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, ERR_SEARCH_UNSUPPORTED_CRITICAL_CONTROL.get(oid)); } } } } } private void processSearch() throws DirectoryException, CanceledOperationException { startECLSessionMsg = new StartECLSessionMsg(); startECLSessionMsg.setECLRequestType( StartECLSessionMsg.REQUEST_TYPE_FROM_COOKIE); startECLSessionMsg.setChangeNumber( new ChangeNumber(TimeThread.getTime(),(short)0, (short)0)); startECLSessionMsg.setCrossDomainServerState(requestCookie.toString()); if (persistentSearch==null) startECLSessionMsg.setPersistent(StartECLSessionMsg.NON_PERSISTENT); else if (!changesOnly) startECLSessionMsg.setPersistent(StartECLSessionMsg.PERSISTENT); else startECLSessionMsg.setPersistent( StartECLSessionMsg.PERSISTENT_CHANGES_ONLY); startECLSessionMsg.setFirstDraftChangeNumber(0); startECLSessionMsg.setLastDraftChangeNumber(0); // Help correlate with access log with the format: "conn=x op=y msgID=z" startECLSessionMsg.setOperationId( "conn="+String.valueOf(this.getConnectionID()) + " op="+String.valueOf(this.getOperationID()) + " msgID="+String.valueOf(getOperationID())); startECLSessionMsg.setExcludedDNs( MultimasterReplication.getPrivateDomains()); // Start session eclSession = replicationServer.createECLSession(startECLSessionMsg); if (!getScope().equals(SearchScope.SINGLE_LEVEL)) { // Root entry Entry entry = createRootEntry(); if (matchFilter(entry)) returnEntry(entry, null); } if (true) { // Loop on result entries int INITIAL=0; int PSEARCH=1; int phase=INITIAL; while (true) { // Check for a request to cancel this operation. checkIfCanceled(false); ECLUpdateMsg update = eclSession.getNextUpdate(); if (update!=null) { if (phase==INITIAL) buildAndReturnEntry(update); } else { if (phase==INITIAL) { if (this.persistentSearch == null) { eclSession.close(); break; } else { phase=PSEARCH; break; } } } } } } private boolean supportsControl(String oid) { return ((supportedControls != null) && supportedControls.contains(oid)); } /** * Build an ECL entry from a provided ECL msg and return it. * @param eclmsg The provided ECL msg. * @throws DirectoryException When an errors occurs. */ private void buildAndReturnEntry(ECLUpdateMsg eclmsg) throws DirectoryException { Entry entry = null; // build and filter entry = createEntryFromMsg(eclmsg); if (matchFilter(entry)) { List<Control> controls = new ArrayList<Control>(0); EntryChangelogNotificationControl clrc = new EntryChangelogNotificationControl( true,eclmsg.getCookie().toString()); controls.add(clrc); boolean entryReturned = returnEntry(entry, controls); if (entryReturned==false) { // FIXME:ECL Handle the error case where returnEntry fails } } } /** * Test if the provided entry matches the filter, base and scope. * @param entry The provided entry * @return whether the entry matches. * @throws DirectoryException When a problem occurs. */ private boolean matchFilter(Entry entry) throws DirectoryException { boolean ms = entry.matchesBaseAndScope(getBaseDN(), getScope()); boolean mf = getFilter().matchesEntry(entry); return (ms && mf); } /** * Create an ECL entry from a provided ECL msg. * * @param eclmsg the provided ECL msg. * @return the created ECL entry. * @throws DirectoryException When an error occurs. */ public static Entry createEntryFromMsg(ECLUpdateMsg eclmsg) throws DirectoryException { Entry clEntry = null; // Get the meat fro the ecl msg UpdateMsg msg = eclmsg.getUpdateMsg(); if (msg instanceof AddMsg) { AddMsg addMsg = (AddMsg)msg; // Map the addMsg to an LDIF string for the 'changes' attribute String LDIFchanges = addMsgToLDIFString(addMsg); clEntry = createChangelogEntry( eclmsg.getServiceId(), eclmsg.getCookie().toString(), DN.decode(addMsg.getDn()), addMsg.getChangeNumber(), LDIFchanges, // entry as created (in LDIF format) addMsg.getUniqueId(), null, // real time current entry null, // real time attrs names null, // hist entry attributes 0, // TODO:ECL G Good changelog draft compat. addMsg.getSeqnum() "add"); } else if (msg instanceof ModifyMsg) { ModifyMsg modMsg = (ModifyMsg)msg; InternalClientConnection conn = InternalClientConnection.getRootConnection(); try { // Map the modMsg modifications to an LDIF string // for the 'changes' attribute of the CL entry ModifyOperation modifyOperation = (ModifyOperation)modMsg.createOperation(conn); String LDIFchanges = modToLDIF(modifyOperation.getModifications()); // TODO:ECL G Good changelog draft compat. Hist entry attributes // ArrayList<RawAttribute> attributes = modMsg.getEntryAttributes(); clEntry = createChangelogEntry( eclmsg.getServiceId(), eclmsg.getCookie().toString(), DN.decode(modMsg.getDn()), modMsg.getChangeNumber(), LDIFchanges, modMsg.getUniqueId(), null, // real time current entry null, // real time attrs names null, // hist entry attributes 0, // TODO:ECL G Good changelog draft compat. modMsg.getSeqnum() "modify"); } catch(Exception e) { // FIXME:ECL Handle error when createOperation raise DataFormatExceptin } } else if (msg instanceof ModifyDNMsg) { ModifyDNMsg modDNMsg = (ModifyDNMsg)msg; clEntry = createChangelogEntry( eclmsg.getServiceId(), eclmsg.getCookie().toString(), DN.decode(modDNMsg.getDn()), modDNMsg.getChangeNumber(), null, modDNMsg.getUniqueId(), null, // real time current entry null, // real time attrs names null, // hist entry attributes 0, // TODO:ECL G Good changelog draft compat. modDNMsg.getSeqnum() "modrdn"); Attribute a = Attributes.create("newrdn", modDNMsg.getNewRDN()); clEntry.addAttribute(a, null); Attribute b = Attributes.create("newsuperior", modDNMsg.getNewSuperior()); clEntry.addAttribute(b, null); Attribute c = Attributes.create("deleteoldrdn", String.valueOf(modDNMsg.deleteOldRdn())); clEntry.addAttribute(c, null); } else if (msg instanceof DeleteMsg) { DeleteMsg delMsg = (DeleteMsg)msg; ArrayList<RawAttribute> rattributes = new ArrayList<RawAttribute>(); String delAttrs = delMsgToLDIFString(rattributes); clEntry = createChangelogEntry( eclmsg.getServiceId(), eclmsg.getCookie().toString(), DN.decode(delMsg.getDn()), delMsg.getChangeNumber(), delAttrs, delMsg.getUniqueId(), null, null, null, //rattributes, 0, // TODO:ECL G Good changelog draft compat. delMsg.getSeqnum() "delete"); } return clEntry; } /** * Creates the root entry of the external changelog. * @return The root entry created. */ private Entry createRootEntry() { HashMap<ObjectClass,String> oclasses = new LinkedHashMap<ObjectClass,String>(3); oclasses.putAll(eclObjectClasses); HashMap<AttributeType,List<Attribute>> userAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); HashMap<AttributeType,List<Attribute>> operationalAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); // Add to the root entry the replication state of each domain // TODO:ECL Put in ECL root entry, the ServerState for each domain AttributeType descType = DirectoryServer.getAttributeType("description"); List<ReplicationDomain> supportedReplicationDomains = new ArrayList<ReplicationDomain>(); for (ReplicationDomain domain : supportedReplicationDomains) { // Crappy stuff to return the server state to the client LinkedList<Attribute> attrList = new LinkedList<Attribute>(); attrList.add(Attributes.create("description", domain.getServiceID() + "/" + domain.getServerState().toString())); userAttrs.put(descType, attrList); } Entry e = new Entry(this.rootBaseDN, oclasses, userAttrs, operationalAttrs); return e; } /** * Create an ECL entry. * @param serviceID The provided cookie value. * @param cookie The provided cookie value. * @param targetDN The provided targetDN. * @param changeNumber The provided replication changeNumber. * @param clearLDIFchanges The provided LDIF changes for ADD and MODIFY * @param targetUUID The provided targetUUID. * @param entry The provided related current entry. * @param targetAttrNames The provided list of attributes names that should * be read from the entry (real time values) * @param histEntryAttributes TODO:ECL Adress hist entry attributes * @param seqnum The provided RCL int change number. * @param changetype The provided change type (add, ...) * @return The created ECL entry. * @throws DirectoryException * When any error occurs. */ public static Entry createChangelogEntry( String serviceID, String cookie, DN targetDN, ChangeNumber changeNumber, String clearLDIFchanges, String targetUUID, Entry entry, List<String> targetAttrNames, List<RawAttribute> histEntryAttributes, int seqnum, String changetype) throws DirectoryException { String dnString = "cn="+ changeNumber +"," + serviceID + "," + ServerConstants.DN_EXTERNAL_CHANGELOG_ROOT; HashMap<ObjectClass,String> oClasses = new LinkedHashMap<ObjectClass,String>(3); oClasses.putAll(eclObjectClasses); ObjectClass extensibleObjectOC = DirectoryServer.getObjectClass(OC_EXTENSIBLE_OBJECT_LC, true); oClasses.put(extensibleObjectOC, OC_EXTENSIBLE_OBJECT); HashMap<AttributeType,List<Attribute>> uAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); HashMap<AttributeType,List<Attribute>> operationalAttrs = new LinkedHashMap<AttributeType,List<Attribute>>(); ArrayList<Attribute> attrList = new ArrayList<Attribute>(1); // TODO:ECL G Good changelog compat. if (seqnum>0) { Attribute a = Attributes.create("changeNumber", String.valueOf(seqnum)); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); } // Attribute a = Attributes.create("replicationCSN", changeNumber.toString()); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); // SimpleDateFormat dateFormat; dateFormat = new SimpleDateFormat(DATE_FORMAT_GMT_TIME); dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); // ?? a = Attributes.create( DirectoryServer.getAttributeType("changeTime", true), dateFormat.format(new Date(changeNumber.getTime()))); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); /* Change time in a friendly format Date date = new Date(changeNumber.getTime()); a = Attributes.create("clearChangeTime", date.toString()); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); */ // a = Attributes.create("changeType", changetype); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); // a = Attributes.create( DirectoryServer.getAttributeType("targetdn", true), targetDN.toNormalizedString()); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); // a = Attributes.create("replicaIdentifier", Short.toString(changeNumber.getServerId())); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); if (clearLDIFchanges != null) { if (changetype.equalsIgnoreCase("delete")) { a = Attributes.create("clearDeletedEntryAttrs", clearLDIFchanges); } attrList = new ArrayList<Attribute>(1); attrList.add(a); operationalAttrs.put(a.getAttributeType(), attrList); if (changetype.equalsIgnoreCase("delete")) { a = Attributes.create("deletedentryattrs", clearLDIFchanges + "\n"); // force base64 } else { AttributeType at = DirectoryServer.getAttributeType("changes"); a = Attributes.create(at, clearLDIFchanges + "\n"); // force base64 } attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); } a = Attributes.create("targetentryuuid", targetUUID); attrList = new ArrayList<Attribute>(1); attrList.add(a); operationalAttrs.put(a.getAttributeType(), attrList); a = Attributes.create("cookie", cookie); attrList = new ArrayList<Attribute>(1); attrList.add(a); operationalAttrs.put(a.getAttributeType(), attrList); // entryAttribute version /* if (targetAttrNames != null) { String sEntryAttrs = null; for (String attrName : targetAttrNames) { List<Attribute> attrs = entry.getAttribute(attrName); for (Attribute attr : attrs) { if (sEntryAttrs==null) sEntryAttrs=""; else sEntryAttrs+=";"; sEntryAttrs += attr.toString(); } } if (sEntryAttrs!=null) { a = Attributes.create("entryAttributes", sEntryAttrs); attrList = new ArrayList<Attribute>(1); attrList.add(a); uAttrs.put(a.getAttributeType(), attrList); } } */ /* if (targetAttrNames != null) { for (String attrName : targetAttrNames) { String newName = "target"+attrName; List<Attribute> attrs = entry.getAttribute(attrName); for (Attribute aa : attrs) { AttributeBuilder builder = new AttributeBuilder( DirectoryServer.getDefaultAttributeType(newName)); builder.setOptions(aa.getOptions()); builder.addAll(aa); attrList = new ArrayList<Attribute>(1); attrList.add(builder.toAttribute()); uAttrs.put(aa.getAttributeType(), attrList); } } } */ /* TODO: Implement entry attributes historical values if (histEntryAttributes != null) { for (RawAttribute rea : histEntryAttributes) { // uAttrs.put(ea.getAttributeType(), null); // FIXME: ERRONEOUS TYPING !!!!!!!!!!!!! try { String newName = "target"+rea.getAttributeType(); AttributeType nat=DirectoryServer.getDefaultAttributeType(newName); rea.setAttributeType(newName); attrList = new ArrayList<Attribute>(1); attrList.add(rea.toAttribute()); uAttrs.put(nat, attrList); } catch(Exception e) { } } } */ // at the end build the CL entry to be returned Entry cle = new Entry( DN.decode(dnString), eclObjectClasses, uAttrs, operationalAttrs); return cle; } private static String addMsgToLDIFString(AddMsg addMsg) { StringBuilder modTypeLine = new StringBuilder(); // LinkedList<StringBuilder> ldifLines = // new LinkedList<StringBuilder>(); try { AddOperation addOperation = (AddOperation)addMsg.createOperation( InternalClientConnection.getRootConnection()); Map<AttributeType,List<Attribute>> attributes = new HashMap<AttributeType,List<Attribute>>(); for (RawAttribute a : addOperation.getRawAttributes()) { Attribute attr = a.toAttribute(); AttributeType attrType = attr.getAttributeType(); List<Attribute> attrs = attributes.get(attrType); if (attrs == null) { attrs = new ArrayList<Attribute>(1); attrs.add(attr); attributes.put(attrType, attrs); } else { attrs.add(attr); } } for (List<Attribute> attrList : attributes.values()) { for (Attribute a : attrList) { StringBuilder attrName = new StringBuilder(a.getName()); for (String o : a.getOptions()) { attrName.append(";"); attrName.append(o); } for (AttributeValue av : a) { String stringValue = av.toString(); modTypeLine.append(attrName); if (needsBase64Encoding(stringValue)) { modTypeLine.append(":: "); modTypeLine.append(Base64.encode(av.getValue())); } else { modTypeLine.append(": "); modTypeLine.append(stringValue); } modTypeLine.append("\n"); } } } return modTypeLine.toString(); } catch(Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } return null; } /** * q. * @param rattributes q. * @return */ private static String delMsgToLDIFString(ArrayList<RawAttribute> rattributes) { StringBuilder modTypeLine = new StringBuilder(); try { Map<AttributeType,List<Attribute>> attributes = new HashMap<AttributeType,List<Attribute>>(); for (RawAttribute a : rattributes) { Attribute attr = a.toAttribute(); AttributeType attrType = attr.getAttributeType(); List<Attribute> attrs = attributes.get(attrType); if (attrs == null) { attrs = new ArrayList<Attribute>(1); attrs.add(attr); attributes.put(attrType, attrs); } else { attrs.add(attr); } } for (List<Attribute> attrList : attributes.values()) { for (Attribute a : attrList) { StringBuilder attrName = new StringBuilder(a.getName()); for (String o : a.getOptions()) { attrName.append(";"); attrName.append(o); } for (AttributeValue av : a) { // ?? String stringValue = av.toString(); modTypeLine.append(attrName); if (needsBase64Encoding(stringValue)) { modTypeLine.append(":: "); modTypeLine.append(Base64.encode(av.getValue())); } else { modTypeLine.append(": "); modTypeLine.append(stringValue); } modTypeLine.append("\n"); } } } return modTypeLine.toString(); } catch(Exception e) { TRACER.debugCaught(DebugLogLevel.ERROR, e); } return null; } /** * Dumps a list of modifications into an LDIF string. * @param mods The provided list of modifications. * @return The LDIF string. */ public static String modToLDIF(List<Modification> mods) { StringBuilder modTypeLine = new StringBuilder(); Iterator<Modification> iterator = mods.iterator(); while (iterator.hasNext()) { Modification m = iterator.next(); Attribute a = m.getAttribute(); String attrName = a.getName(); modTypeLine.append(m.getModificationType().getLDIFName()); modTypeLine.append(": "); modTypeLine.append(attrName); modTypeLine.append("\n"); Iterator<AttributeValue> iteratorValues = a.iterator(); while (iteratorValues.hasNext()) { AttributeValue av = iteratorValues.next(); String stringValue = av.toString(); modTypeLine.append(attrName); if (needsBase64Encoding(stringValue)) { modTypeLine.append(":: "); modTypeLine.append(Base64.encode(av.getValue())); } else { modTypeLine.append(": "); modTypeLine.append(stringValue); } modTypeLine.append("\n"); } modTypeLine.append("-"); if (iterator.hasNext()) { modTypeLine.append("\n"); } } return modTypeLine.toString(); } /** * {@inheritDoc} */ public CancelResult cancel(CancelRequest cancelRequest) { if (eclSession!=null) { try { eclSession.close(); } catch(Exception e){} } return super.cancel(cancelRequest); } } opends/src/server/org/opends/server/workflowelement/externalchangelog/ECLWorkflowElement.java
New file @@ -0,0 +1,220 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.workflowelement.externalchangelog; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import org.opends.server.admin.std.server.WorkflowElementCfg; import org.opends.server.core.DirectoryServer; import org.opends.server.core.PersistentSearch; import org.opends.server.core.SearchOperation; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.replication.server.ReplicationServer; import org.opends.server.types.CanceledOperationException; import org.opends.server.types.DirectoryException; import org.opends.server.types.Operation; import org.opends.server.workflowelement.LeafWorkflowElement; /** * This class defines a workflow element for the external changelog (ECL); * e-g an entity that handles the processing of an operation against the ECL. */ public class ECLWorkflowElement extends LeafWorkflowElement<WorkflowElementCfg> { /** * The tracer object for the debug logger. */ private static final DebugTracer TRACER = getTracer(); /** *The set of persistent searches registered with this work flow element. */ private final List<PersistentSearch> persistentSearches = new CopyOnWriteArrayList<PersistentSearch>(); /** * A string indicating the type of the workflow element. */ public static final String ECL_WORKFLOW_ELEMENT = "EXTERNAL CHANGE LOG"; /** * The replication server object to which we will submits request * on the ECL. Retrieved from the local DirectoryServer. */ private ReplicationServer replicationServer; /** * Creates a new instance of the External Change Log workflow element. * @param rs the provided replication server * @throws DirectoryException If the ECL workflow is already registered. */ public ECLWorkflowElement(ReplicationServer rs) throws DirectoryException { this.replicationServer =rs; super.initialize(ECL_WORKFLOW_ELEMENT, ECL_WORKFLOW_ELEMENT); DirectoryServer.registerWorkflowElement(this); } /** * {@inheritDoc} */ @Override public void finalizeWorkflowElement() { // null all fields so that any use of the finalized object will raise // an NPE super.initialize(ECL_WORKFLOW_ELEMENT, null); // Cancel all persistent searches. for (PersistentSearch psearch : persistentSearches) { psearch.cancel(); } persistentSearches.clear(); } /** * {@inheritDoc} */ public void execute(Operation operation) throws CanceledOperationException { switch (operation.getOperationType()) { case SEARCH: ECLSearchOperation searchOperation = new ECLSearchOperation((SearchOperation) operation); searchOperation.processECLSearch(this); break; case ABANDON: // There is no processing for an abandon operation. break; case BIND: case ADD: case DELETE: case MODIFY: case MODIFY_DN: case COMPARE: default: throw new AssertionError("Attempted to execute an invalid operation " + "type: " + operation.getOperationType() + " (" + operation + ")"); } } /** * Attaches the current local operation to the global operation so that * operation runner can execute local operation post response later on. * * @param <O> subtype of Operation * @param <L> subtype of LocalBackendOperation * @param globalOperation the global operation to which local operation * should be attached to * @param currentLocalOperation the local operation to attach to the global * operation */ @SuppressWarnings("unchecked") public static final <O extends Operation,L> void attachLocalOperation (O globalOperation, L currentLocalOperation) { List<?> existingAttachment = (List<?>) globalOperation.getAttachment(Operation.LOCALBACKENDOPERATIONS); List<L> newAttachment = new ArrayList<L>(); if (existingAttachment != null) { // This line raises an unchecked conversion warning. // There is nothing we can do to prevent this warning // so let's get rid of it since we know the cast is safe. newAttachment.addAll ((List<L>) existingAttachment); } newAttachment.add (currentLocalOperation); globalOperation.setAttachment(Operation.LOCALBACKENDOPERATIONS, newAttachment); } /** * Registers the provided persistent search operation with this * local backend workflow element so that it will be notified of any * add, delete, modify, or modify DN operations that are performed. * * @param persistentSearch * The persistent search operation to register with this * local backend workflow element. */ void registerPersistentSearch(PersistentSearch persistentSearch) { PersistentSearch.CancellationCallback callback = new PersistentSearch.CancellationCallback() { public void persistentSearchCancelled(PersistentSearch psearch) { psearch.getSearchOperation().cancel(null); persistentSearches.remove(psearch); } }; persistentSearches.add(persistentSearch); persistentSearch.registerCancellationCallback(callback); } /** * Gets the list of persistent searches currently active against * this local backend workflow element. * * @return The list of persistent searches currently active against * this local backend workflow element. */ public List<PersistentSearch> getPersistentSearches() { return persistentSearches; } /** * Returns the associated replication server. * @return the rs. */ public ReplicationServer getReplicationServer() { return this.replicationServer; } } opends/src/server/org/opends/server/workflowelement/externalchangelog/package-info.java
New file @@ -0,0 +1,38 @@ /* * 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 2009 Sun Microsystems, Inc. */ /** * This package contains source for the local backend workflow element, which * are used to process operations against data stored in local backend databases * and other repositories that are considered "local". */ @org.opends.server.types.PublicAPI( stability=org.opends.server.types.StabilityLevel.PRIVATE) package org.opends.server.workflowelement.externalchangelog; opends/tests/unit-tests-testng/src/server/org/opends/server/controls/ExternalChangelogControlTest.java
New file @@ -0,0 +1,100 @@ /* * 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 2009 Sun Microsystems, Inc. */ package org.opends.server.controls; import static org.opends.server.util.ServerConstants.OID_ECL_COOKIE_EXCHANGE_CONTROL; import static org.opends.server.util.ServerConstants.OID_PERSISTENT_SEARCH; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import org.opends.server.protocols.asn1.ASN1; import org.opends.server.protocols.asn1.ASN1Writer; import org.opends.server.protocols.ldap.LDAPControl; import org.opends.server.protocols.ldap.LDAPReader; import org.opends.server.replication.common.MultiDomainServerState; import org.opends.server.types.ByteString; import org.opends.server.types.ByteStringBuilder; import org.opends.server.types.DirectoryException; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; /** * Test ChangeNumber and ChangeNumberGenerator */ public class ExternalChangelogControlTest extends ControlsTestCase { /** * Create values for External Changelog Request Control */ @DataProvider(name = "eclRequestControl") public Object[][] createECLRequestControlTest() { return new Object[][] { {true, "" }, {false, "o=test:;" }, {false, "o=test:000001210b6f21e904b100000002;" }, {false, "o=test:000001210b6f21e904b100000001;o=test2:000001210b6f21e904b100000002;" }, {false, "o=test:000001210b6f21e904b100000001 000001210b6f21e904b200000001;o=test2:000001210b6f21e904b100000002 000001210b6f21e904b200000002;" }, }; } /** * Test ExternalChangelogRequestControl */ @Test(dataProvider = "eclRequestControl") public void checkECLRequestControlTest(boolean critical, String value) throws Exception { // Test contructor MultiDomainServerState mdss = new MultiDomainServerState(value); ExternalChangelogRequestControl eclrc = new ExternalChangelogRequestControl(critical, mdss); assertNotNull(eclrc); assertEquals(critical, eclrc.isCritical()); assertEquals(OID_ECL_COOKIE_EXCHANGE_CONTROL, eclrc.getOID()); assertTrue(eclrc.getCookie().equalsTo(mdss)); // Test encode/decode ByteStringBuilder bsb = new ByteStringBuilder(); ASN1Writer writer = ASN1.getWriter(bsb); bsb.clear(); eclrc.write(writer); LDAPControl control = LDAPReader.readControl(ASN1.getReader(bsb)); eclrc = ExternalChangelogRequestControl.DECODER.decode(control.isCritical(), control.getValue()); assertNotNull(eclrc); assertEquals(critical, eclrc.isCritical()); assertEquals(OID_ECL_COOKIE_EXCHANGE_CONTROL, eclrc.getOID()); assertTrue(eclrc.getCookie().equalsTo(mdss), "Expect:"+value+", Got:"+eclrc.getCookie().toString()); } } opends/tests/unit-tests-testng/src/server/org/opends/server/replication/ReplicationTestCase.java
@@ -158,11 +158,6 @@ // This test suite depends on having the schema available. TestCaseUtils.startServer(); // After start of server because not seen if server not started // after server started once, TestCaseUtils.startServer() does nothing. logError(Message.raw(Category.SYNC, Severity.NOTICE, " ##### Calling ReplicationTestCase.setUp ##### ")); // Initialize the test backend (TestCaseUtils.TEST_ROOT_DN_STRING) // (in case previous (non replication?) tests were run before...) TestCaseUtils.initializeTestBackend(true); @@ -265,7 +260,60 @@ return broker; } /** /** * Open an ECL replicationServer session to the local ReplicationServer protected ReplicationBroker openECLReplicationSession( int window_size, int port, int timeout, boolean emptyOldChanges, Short serverId) throws Exception, SocketException { ServerState state = new ServerState(); //if (emptyOldChanges) // new PersistentServerState(baseDn, serverId, new ServerState()); ReplicationBroker broker = new ReplicationBroker(null, state, "cn=changelog", serverId, window_size, -1, 100000, getReplSessionSecurity(), (byte)1); ArrayList<String> servers = new ArrayList<String>(1); servers.add("localhost:" + port); broker.start(servers); if (timeout != 0) broker.setSoTimeout(timeout); checkConnection(30, broker, port); // give some time to the broker to connect // to the replicationServer. if (emptyOldChanges) { // loop receiving update until there is nothing left // to make sure that message from previous tests have been consumed. try { while (true) { ReplicationMsg rMsg = broker.receive(); if (rMsg instanceof ErrorMsg) { ErrorMsg eMsg = (ErrorMsg)rMsg; logError(new MessageBuilder( "ReplicationTestCase/openReplicationSession ").append( " received ErrorMessage when emptying old changes ").append( eMsg.getDetails()).toMessage()); } } } catch (Exception e) { logError(new MessageBuilder( "ReplicationTestCase/openReplicationSession ").append(e.getMessage()) .append(" when emptying old changes").toMessage()); } } return broker; } */ /** * Check connection of the provided ds to the * replication server. Waits for connection to be ok up to secTimeout seconds * before failing. opends/tests/unit-tests-testng/src/server/org/opends/server/replication/SchemaReplicationTest.java
@@ -22,7 +22,7 @@ * CDDL HEADER END * * * Copyright 2008 Sun Microsystems, Inc. * Copyright 2008-2009 Sun Microsystems, Inc. */ package org.opends.server.replication; @@ -210,7 +210,7 @@ * Checks that changes to the schema pushed to the replicationServer * are received and correctly replayed by replication plugin. */ @Test(dependsOnMethods = { "pushSchemaChange" }) @Test(enabled=true,dependsOnMethods = { "pushSchemaChange" }) public void replaySchemaChange() throws Exception { logError(Message.raw(Category.SYNC, Severity.NOTICE, @@ -341,5 +341,7 @@ { broker.stop(); } logError(Message.raw(Category.SYNC, Severity.NOTICE, "Ending replication test : pushSchemaFilesChange ")); } } opends/tests/unit-tests-testng/src/server/org/opends/server/replication/common/ExternalChangeLogTest.java
New file @@ -0,0 +1,1569 @@ /* * 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-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.common; import static org.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING; import static org.opends.server.loggers.ErrorLogger.logError; import static org.opends.server.loggers.debug.DebugLogger.getTracer; import static org.opends.server.replication.protocol.OperationContext.SYNCHROCONTEXT; import static org.opends.server.util.StaticUtils.createEntry; import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.net.ServerSocket; import java.net.Socket; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.SortedSet; import java.util.TreeSet; import org.opends.messages.Category; import org.opends.messages.Message; import org.opends.messages.Severity; import org.opends.server.TestCaseUtils; import org.opends.server.api.Backend; import org.opends.server.api.ConnectionHandler; import org.opends.server.api.SynchronizationProvider; import org.opends.server.backends.MemoryBackend; import org.opends.server.config.ConfigException; import org.opends.server.controls.ExternalChangelogRequestControl; import org.opends.server.controls.PersistentSearchChangeType; import org.opends.server.controls.PersistentSearchControl; import org.opends.server.core.AddOperationBasis; import org.opends.server.core.DirectoryServer; import org.opends.server.core.ModifyDNOperationBasis; import org.opends.server.loggers.debug.DebugTracer; import org.opends.server.plugins.InvocationCounterPlugin; import org.opends.server.protocols.asn1.ASN1Exception; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.protocols.internal.InternalSearchOperation; import org.opends.server.protocols.ldap.BindRequestProtocolOp; import org.opends.server.protocols.ldap.BindResponseProtocolOp; import org.opends.server.protocols.ldap.LDAPAttribute; import org.opends.server.protocols.ldap.LDAPConnectionHandler; import org.opends.server.protocols.ldap.LDAPConstants; import org.opends.server.protocols.ldap.LDAPFilter; import org.opends.server.protocols.ldap.LDAPMessage; import org.opends.server.protocols.ldap.LDAPResultCode; import org.opends.server.protocols.ldap.LDAPStatistics; import org.opends.server.protocols.ldap.SearchRequestProtocolOp; import org.opends.server.protocols.ldap.SearchResultDoneProtocolOp; import org.opends.server.protocols.ldap.SearchResultEntryProtocolOp; import org.opends.server.replication.ReplicationTestCase; import org.opends.server.replication.plugin.DomainFakeCfg; import org.opends.server.replication.plugin.LDAPReplicationDomain; import org.opends.server.replication.plugin.MultimasterReplication; import org.opends.server.replication.protocol.AddMsg; import org.opends.server.replication.protocol.DeleteMsg; import org.opends.server.replication.protocol.DoneMsg; import org.opends.server.replication.protocol.ECLUpdateMsg; import org.opends.server.replication.protocol.ModifyDNMsg; import org.opends.server.replication.protocol.ModifyDnContext; import org.opends.server.replication.protocol.ModifyMsg; import org.opends.server.replication.protocol.ReplicationMsg; import org.opends.server.replication.protocol.UpdateMsg; import org.opends.server.replication.server.ReplServerFakeConfiguration; import org.opends.server.replication.server.ReplicationServer; import org.opends.server.replication.server.ReplicationServerDomain; import org.opends.server.replication.service.ReplicationBroker; import org.opends.server.tools.LDAPWriter; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeValue; import org.opends.server.types.Attributes; import org.opends.server.types.ByteString; import org.opends.server.types.Control; import org.opends.server.types.DN; import org.opends.server.types.DereferencePolicy; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.InitializationException; import org.opends.server.types.LDAPException; import org.opends.server.types.LDIFExportConfig; import org.opends.server.types.Modification; import org.opends.server.types.ModificationType; import org.opends.server.types.RDN; import org.opends.server.types.ResultCode; import org.opends.server.types.SearchResultEntry; import org.opends.server.types.SearchScope; import org.opends.server.util.LDIFWriter; import org.opends.server.util.TimeThread; import org.opends.server.workflowelement.localbackend.LocalBackendModifyDNOperation; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; /** * Tests for the replicationServer code. */ public class ExternalChangeLogTest extends ReplicationTestCase { // The tracer object for the debug logger private static final DebugTracer TRACER = getTracer(); // The replicationServer that will be used in this test. private ReplicationServer replicationServer = null; // The port of the replicationServer. private int replicationServerPort; public static final String TEST_ROOT_DN_STRING2 = "o=test2"; public static final String TEST_BACKEND_ID2 = "test2"; // The LDAPStatistics object associated with the LDAP connection handler. protected LDAPStatistics ldapStatistics; /** * Set up the environment for performing the tests in this Class. * Replication * * @throws Exception * If the environment could not be set up. */ @BeforeClass public void setUp() throws Exception { super.setUp(); // This test suite depends on having the schema available. configure(); } /** * Utility : configure a replicationServer. */ protected void configure() throws Exception { // Find a free port for the replicationServer ServerSocket socket = TestCaseUtils.bindFreePort(); replicationServerPort = socket.getLocalPort(); socket.close(); /* // Add the replication server to the configuration TestCaseUtils.dsconfig( "create-replication-server", "--provider-name", "Multimaster Synchronization", "--set", "replication-db-directory:" + "externalChangeLogTestConfigureDb", "--set", "replication-port:" + replicationServerPort, "--set", "replication-server-id:71"); // Retrieves the replicationServer object DirectoryServer.getSynchronizationProviders(); for (SynchronizationProvider<?> provider : DirectoryServer .getSynchronizationProviders()) { if (provider instanceof MultimasterReplication) { MultimasterReplication mmp = (MultimasterReplication) provider; ReplicationServerListener list = mmp.getReplicationServerListener(); if (list != null) { replicationServer = list.getReplicationServer(); if (replicationServer != null) { break; } } } } */ ReplServerFakeConfiguration conf1 = new ReplServerFakeConfiguration( replicationServerPort, "ExternalChangeLogTestDb", 0, 71, 0, 100, null); replicationServer = new ReplicationServer(conf1);; debugInfo("configure", "ReplicationServer created"+replicationServer); } /** * Launcher. */ @Test(enabled=true) public void ECLReplicationServerTest() { ECLRemoteEmpty();replicationServer.clearDb(); ECLDirectEmpty();replicationServer.clearDb(); ECLDirectAllOps();replicationServer.clearDb(); ECLRemoteNonEmpty();replicationServer.clearDb(); ECLDirectMsg();replicationServer.clearDb(); ECLDirectPsearch(true);replicationServer.clearDb(); ECLDirectPsearch(false);replicationServer.clearDb(); ECLPrivateDirectMsg();replicationServer.clearDb(); // TODO:ECL Test SEARCH abandon and check everything shutdown and cleaned // TODO:ECL Test PSEARCH abandon and check everything shutdown and cleaned // TODO:ECL Test invalid DN in cookie returns UNWILLING + message // TODO:ECL Test notif control returned contains the cookie // TODO:ECL Test the attributes list and values returned in ECL entries // TODO:ECL Test search -s base, -s one } //======================================================= // From 3 remote ECL session, // Test DoneMsg is received from 1 suffix on each session. private void ECLRemoteEmpty() { String tn = "ECLRemoteEmpty"; debugInfo(tn, "Starting test\n\n"); ReplicationBroker server1 = null; ReplicationBroker server2 = null; ReplicationBroker server3 = null; try { // Create 3 ECL broker server1 = openReplicationSession( DN.decode("cn=changelog"), (short)1111, 100, replicationServerPort, 1000, false); assertTrue(server1.isConnected()); server2 = openReplicationSession( DN.decode("cn=changelog"), (short)2222, 100, replicationServerPort,1000, false); assertTrue(server2.isConnected()); server3 = openReplicationSession( DN.decode("cn=changelog"), (short)3333, 100, replicationServerPort,1000, false); assertTrue(server3.isConnected()); // Test broker1 receives only Done ReplicationMsg msg; int msgc=0; do { msg = server1.receive(); msgc++; } while(!(msg instanceof DoneMsg)); assertTrue(msgc==1, "Ending " + tn + " with incorrect message type :" + msg.getClass().getCanonicalName()); assertTrue(msg instanceof DoneMsg); // Test broker2 receives only Done msgc=0; do { msg = server2.receive(); msgc++; } while(!(msg instanceof DoneMsg)); assertTrue(msgc==1, "Ending " + tn + " with incorrect message type :" + msg.getClass().getCanonicalName()); // Test broker3 receives only Done msgc=0; do { msg = server3.receive(); msgc++; } while(!(msg instanceof DoneMsg)); assertTrue(msgc==1, "Ending " + tn + " with incorrect message type :" + msg.getClass().getCanonicalName()); server1.stop(); server2.stop(); server3.stop(); sleep(500); debugInfo(tn, "Ending test successfully\n\n"); } catch(Exception e) { debugInfo(tn, "Ending test with exception:" + stackTraceToSingleLineString(e)); assertTrue(e == null); } } //======================================================= // From 1 remote ECL session, // test simple update to be received from 2 suffixes private void ECLRemoteNonEmpty() { String tn = "ECLRemoteNonEmpty"; debugInfo(tn, "Starting test\n\n"); replicationServer.clearDb(); // create a broker ReplicationBroker server01 = null; ReplicationBroker server02 = null; ReplicationBroker serverECL = null; try { // create 2 reguler brokers on the 2 suffixes server01 = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING), (short) 1201, 100, replicationServerPort, 1000, true); server02 = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING2), (short) 1202, 100, replicationServerPort, 1000, true, EMPTY_DN_GENID); // create and publish 1 change on each suffix long time = TimeThread.getTime(); int ts = 1; ChangeNumber cn1 = new ChangeNumber(time, ts++, (short)1201); DeleteMsg delMsg1 = new DeleteMsg("o=" + tn + "1," + TEST_ROOT_DN_STRING, cn1, "ECLBasicMsg1uid"); server01.publish(delMsg1); debugInfo(tn, "publishes:" + delMsg1); ChangeNumber cn2 = new ChangeNumber(time, ts++, (short)1202); DeleteMsg delMsg2 = new DeleteMsg("o=" + tn + "2," + TEST_ROOT_DN_STRING2, cn2, "ECLBasicMsg2uid"); server02.publish(delMsg2); debugInfo(tn, "publishes:" + delMsg2); // wait for the server to take these changes into account sleep(500); // open ECL broker serverECL = openReplicationSession( DN.decode("cn=changelog"), (short)10, 100, replicationServerPort, 1000, false); assertTrue(serverECL.isConnected()); // receive change 1 from suffix 1 ReplicationMsg msg; msg = serverECL.receive(); ECLUpdateMsg eclu = (ECLUpdateMsg)msg; UpdateMsg u = eclu.getUpdateMsg(); debugInfo(tn, "RESULT:" + u.getChangeNumber()); assertTrue(u.getChangeNumber().equals(cn1), "RESULT:" + u.getChangeNumber()); assertTrue(eclu.getCookie().equalsTo(new MultiDomainServerState( "o=test:"+delMsg1.getChangeNumber()+";"))); // receive change 2 from suffix 2 msg = serverECL.receive(); eclu = (ECLUpdateMsg)msg; u = eclu.getUpdateMsg(); debugInfo(tn, "RESULT:" + u.getChangeNumber()); assertTrue(u.getChangeNumber().equals(cn2), "RESULT:" + u.getChangeNumber()); assertTrue(eclu.getCookie().equalsTo(new MultiDomainServerState( "o=test2:"+delMsg2.getChangeNumber()+";"+ "o=test:"+delMsg1.getChangeNumber()+";"))); // receive Done msg = serverECL.receive(); debugInfo(tn, "RESULT:" + msg); assertTrue(msg instanceof DoneMsg, "RESULT:" + msg); // clean serverECL.stop(); server01.stop(); server02.stop(); sleep(2000); debugInfo(tn, "Ending test successfully"); } catch(Exception e) { debugInfo(tn, "Ending test with exception:" + stackTraceToSingleLineString(e)); assertTrue(e == null); } } /** * From embedded ECL (no remote session) * With empty RS, simple search should return only root entry. */ private void ECLDirectEmpty() { String tn = "ECLDirectEmpty"; debugInfo(tn, "Starting test\n\n"); try { // search on 'cn=changelog' InternalSearchOperation op = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, LDAPFilter.decode("(objectclass=*)")); debugInfo(tn, "Res code=" + op.getResultCode()); debugInfo(tn, "Res OE=" + ResultCode.OPERATIONS_ERROR); // Error because no cookie assertEquals( op.getResultCode(), ResultCode.OPERATIONS_ERROR, op.getErrorMessage().toString()); // search on 'cn=changelog' InternalSearchOperation op2 = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0, false, LDAPFilter.decode("(objectclass=*)"), new LinkedHashSet<String>(0), getControls(""), null); // success assertEquals( op2.getResultCode(), ResultCode.SUCCESS, op2.getErrorMessage().toString()); // root entry returned assertEquals(op2.getEntriesSent(), 1); debugInfo(tn, "Ending test successfully"); } catch(LDAPException e) { debugInfo(tn, "Ending test with exception e=" + stackTraceToSingleLineString(e)); assertTrue(e == null); } } private ArrayList<Control> getControls(String cookie) { ExternalChangelogRequestControl control = new ExternalChangelogRequestControl(true, new MultiDomainServerState(cookie)); ArrayList<Control> controls = new ArrayList<Control>(0); controls.add(control); return controls; } // Utility - creates an LDIFWriter to dump result entries private static LDIFWriter getLDIFWriter() { LDIFWriter ldifWriter = null; ByteArrayOutputStream stream = new ByteArrayOutputStream(); LDIFExportConfig exportConfig = new LDIFExportConfig(stream); try { ldifWriter = new LDIFWriter(exportConfig); } catch (Exception e) { assert(e==null); } return ldifWriter; } // Add an entry in the database private void addEntry(Entry entry) throws Exception { AddOperationBasis addOp = new AddOperationBasis(connection, InternalClientConnection.nextOperationID(), InternalClientConnection .nextMessageID(), null, entry.getDN(), entry.getObjectClasses(), entry.getUserAttributes(), entry.getOperationalAttributes()); addOp.setInternalOperation(true); addOp.run(); assertEquals(addOp.getResultCode(), ResultCode.SUCCESS, addOp.getErrorMessage().toString() + addOp.getAdditionalLogMessage()); assertNotNull(getEntry(entry.getDN(), 1000, true)); } //======================================================= // From embebbded ECL // Search ECL with 1 domain, public then private backend private void ECLPrivateDirectMsg() { ReplicationServer replServer = null; String tn = "ECLPrivateDirectMsg"; debugInfo(tn, "Starting test"); try { // Initialize a second test backend Backend backend2 = initializeTestBackend2(false); DN baseDn = DN.decode(TEST_ROOT_DN_STRING2); // configure and start replication of TEST_ROOT_DN_STRING on the server SortedSet<String> replServers = new TreeSet<String>(); replServers.add("localhost:"+replicationServerPort); DomainFakeCfg domainConf = new DomainFakeCfg(baseDn, (short) 1602, replServers); LDAPReplicationDomain domain = MultimasterReplication.createNewDomain(domainConf); SynchronizationProvider replicationPlugin = new MultimasterReplication(); replicationPlugin.completeSynchronizationProvider(); sleep(1000); Entry e = createEntry(baseDn); addEntry(e); sleep(1000); ExternalChangelogRequestControl control = new ExternalChangelogRequestControl(true, new MultiDomainServerState("")); ArrayList<Control> controls = new ArrayList<Control>(0); controls.add(control); // search on 'cn=changelog' LinkedHashSet<String> attributes = new LinkedHashSet<String>(); attributes.add("+"); attributes.add("*"); InternalSearchOperation searchOp = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, // Size limit 0, // Time limit false, // Types only LDAPFilter.decode("(targetDN=*)"), attributes, controls, null); assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS, searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage()); LinkedList<SearchResultEntry> entries = searchOp.getSearchEntries(); assertTrue(entries != null); if (entries != null) { for (SearchResultEntry resultEntry : entries) { debugInfo(tn, "Result private entry=" + resultEntry.toLDIFString()); } } assertEquals(entries.size(),1, "Entries number returned by search"); // Same with private backend domain.getBackend().setPrivateBackend(true); searchOp = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, // Size limit 0, // Time limit false, // Types only LDAPFilter.decode("(targetDN=*)"), attributes, controls, null); assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS, searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage()); entries = searchOp.getSearchEntries(); assertTrue(entries != null); assertTrue(entries.size()==0); if (replServer != null) replServer.remove(); if (domain != null) MultimasterReplication.deleteDomain(baseDn); if (replicationPlugin != null) DirectoryServer.deregisterSynchronizationProvider(replicationPlugin); removeTestBackend2(backend2); } catch(Exception e) { debugInfo(tn, "Ending test ECLDirectMsg with exception:\n" + stackTraceToSingleLineString(e)); assertTrue(e == null); } debugInfo(tn, "Ending test successfully"); } //======================================================= // From embebbded ECL // Search ECL with 4 messages on 2 suffixes from 2 brokers private void ECLDirectMsg() { String tn = "ECLDirectMsg"; debugInfo(tn, "Starting test"); try { // Initialize a second test backend Backend backend2 = initializeTestBackend2(true); // LDIFWriter ldifWriter = getLDIFWriter(); // -- ReplicationBroker s1test = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING), (short) 1201, 100, replicationServerPort, 1000, true); ReplicationBroker s2test2 = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING2), (short) 1202, 100, replicationServerPort, 1000, true, EMPTY_DN_GENID); sleep(500); // Produce updates long time = TimeThread.getTime(); int ts = 1; ChangeNumber cn = new ChangeNumber(time, ts++, s1test.getServerId()); DeleteMsg delMsg = new DeleteMsg("uid="+tn+"1," + TEST_ROOT_DN_STRING, cn, tn+"uuid1"); s1test.publish(delMsg); debugInfo(tn, " publishes " + delMsg.getChangeNumber()); cn = new ChangeNumber(time++, ts++, s2test2.getServerId()); delMsg = new DeleteMsg("uid="+tn+"2," + TEST_ROOT_DN_STRING2, cn, tn+"uuid2"); s2test2.publish(delMsg); debugInfo(tn, " publishes " + delMsg.getChangeNumber()); cn = new ChangeNumber(time++, ts++, s2test2.getServerId()); delMsg = new DeleteMsg("uid="+tn+"3," + TEST_ROOT_DN_STRING2, cn, tn+"uuid3"); s2test2.publish(delMsg); debugInfo(tn, " publishes " + delMsg.getChangeNumber()); cn = new ChangeNumber(time++, ts++, s1test.getServerId()); delMsg = new DeleteMsg("uid="+tn+"4," + TEST_ROOT_DN_STRING, cn, tn+"uuid4"); s1test.publish(delMsg); debugInfo(tn, " publishes " + delMsg.getChangeNumber()); sleep(500); // Changes are : // s1 s2 // o=test msg1/msg4 // o=test2 msg2/msg2 String cookie= ""; debugInfo(tn, "STEP 1 - from empty cookie("+cookie+")"); // search on 'cn=changelog' LinkedHashSet<String> attributes = new LinkedHashSet<String>(); attributes.add("+"); attributes.add("*"); InternalSearchOperation searchOp = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, // Size limit 0, // Time limit false, // Types only LDAPFilter.decode("(targetDN=*direct*)"), attributes, getControls(cookie), null); // We expect SUCCESS and the 4 changes assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS, searchOp.getErrorMessage().toString()); LinkedList<SearchResultEntry> entries = searchOp.getSearchEntries(); if (entries != null) { int i=0; for (SearchResultEntry entry : entries) { debugInfo(tn, " RESULT entry returned:" + entry.toSingleLineString()); ldifWriter.writeEntry(entry); try { if (i++==2) cookie = entry.getAttribute("cookie").get(0).iterator().next().toString(); } catch(NullPointerException e) {} } } assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS); // We expect the 4 changes assertEquals(searchOp.getSearchEntries().size(), 4); // Now start from last cookie and expect to get the last change // search on 'cn=changelog' attributes = new LinkedHashSet<String>(); attributes.add("+"); attributes.add("*"); debugInfo(tn, "STEP 2 - from cookie" + cookie); ExternalChangelogRequestControl control = new ExternalChangelogRequestControl(true, new MultiDomainServerState(cookie)); ArrayList<Control> controls = new ArrayList<Control>(0); controls.add(control); searchOp = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, // Size limit 0, // Time limit false, // Types only LDAPFilter.decode("(targetDN=*direct*)"), attributes, controls, null); // We expect SUCCESS and the 4th change assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS, searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage()); cookie= ""; entries = searchOp.getSearchEntries(); if (entries != null) { for (SearchResultEntry entry : entries) { debugInfo(tn, "Result entry=\n" + entry.toLDIFString()); ldifWriter.writeEntry(entry); try { cookie = entry.getAttribute("cookie").get(0).iterator().next().toString(); } catch(NullPointerException e) {} } } assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS); // we expect msg4 assertEquals(searchOp.getSearchEntries().size(), 1); debugInfo(tn, "STEP 3 - from cookie" + cookie); cn = new ChangeNumber(time++, ts++, s1test.getServerId()); delMsg = new DeleteMsg("uid="+tn+"5," + TEST_ROOT_DN_STRING, cn, tn+"uuid5"); s1test.publish(delMsg); sleep(500); // Changes are : // s1 s2 // o=test msg1,msg5 msg4 // o=test2 msg3 msg2 control = new ExternalChangelogRequestControl(true, new MultiDomainServerState(cookie)); controls = new ArrayList<Control>(0); controls.add(control); searchOp = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, // Size limit 0, // Time limit false, // Types only LDAPFilter.decode("(targetDN=*direct*)"), attributes, controls, null); assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS, searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage()); cookie= ""; entries = searchOp.getSearchEntries(); if (entries != null) { for (SearchResultEntry resultEntry : entries) { debugInfo(tn, "Result entry=\n" + resultEntry.toLDIFString()); ldifWriter.writeEntry(resultEntry); try { cookie = resultEntry.getAttribute("cookie").get(0).iterator().next().toString(); } catch(NullPointerException e) {} } } assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS); // we expect 1 entries : msg5 assertEquals(searchOp.getSearchEntries().size(), 1); cookie=""; debugInfo(tn, "STEP 4 - [filter:o=test cookie:" + cookie + "]"); control = new ExternalChangelogRequestControl(true, new MultiDomainServerState(cookie)); controls = new ArrayList<Control>(0); controls.add(control); searchOp = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, // Size limit 0, // Time limit false, // Types only LDAPFilter.decode("(targetDN=*direct*,o=test)"), attributes, controls, null); assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS, searchOp.getErrorMessage().toString() + searchOp.getAdditionalLogMessage()); entries = searchOp.getSearchEntries(); if (entries != null) { for (SearchResultEntry resultEntry : entries) { debugInfo(tn, "Result entry=\n" + resultEntry.toLDIFString()); ldifWriter.writeEntry(resultEntry); try { cookie = resultEntry.getAttribute("cookie").get(0).iterator().next().toString(); } catch(NullPointerException e) {} } } assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS); // we expect msg1 + msg4 + msg5 assertEquals(searchOp.getSearchEntries().size(), 3); // -- ReplicationBroker s1test2 = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING2), (short) 1203, 100, replicationServerPort, 1000, true, EMPTY_DN_GENID); ReplicationBroker s2test = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING), (short) 1204, 100, replicationServerPort, 1000, true); sleep(500); // Test startState of the domain for the first cookie feature time = TimeThread.getTime(); cn = new ChangeNumber(time++, ts++, s1test2.getServerId()); delMsg = new DeleteMsg("uid="+tn+"6," + TEST_ROOT_DN_STRING2, cn, tn+"uuid6"); s1test2.publish(delMsg); cn = new ChangeNumber(time++, ts++, s2test.getServerId()); delMsg = new DeleteMsg("uid="+tn+"7," + TEST_ROOT_DN_STRING, cn, tn+"uuid7"); s2test.publish(delMsg); cn = new ChangeNumber(time++, ts++, s1test2.getServerId()); delMsg = new DeleteMsg("uid="+tn+"8," + TEST_ROOT_DN_STRING2, cn, tn+"uuid8"); s1test2.publish(delMsg); cn = new ChangeNumber(time++, ts++, s2test.getServerId()); delMsg = new DeleteMsg("uid="+tn+"9," + TEST_ROOT_DN_STRING, cn, tn+"uuid9"); s2test.publish(delMsg); sleep(500); ReplicationServerDomain rsd = replicationServer.getReplicationServerDomain(TEST_ROOT_DN_STRING, false); ServerState startState = rsd.getStartState(); assertTrue(startState.getMaxChangeNumber(s1test.getServerId()).getSeqnum()==1); assertTrue(startState.getMaxChangeNumber(s2test.getServerId()).getSeqnum()==7); rsd = replicationServer.getReplicationServerDomain(TEST_ROOT_DN_STRING2, false); startState = rsd.getStartState(); assertTrue(startState.getMaxChangeNumber(s2test2.getServerId()).getSeqnum()==2); assertTrue(startState.getMaxChangeNumber(s1test2.getServerId()).getSeqnum()==6); s1test.stop(); s1test2.stop(); s2test.stop(); s2test2.stop(); removeTestBackend2(backend2); } catch(Exception e) { debugInfo(tn, "Ending test ECLDirectMsg with exception:\n" + stackTraceToSingleLineString(e)); assertTrue(e == null); } debugInfo(tn, "Ending test successfully"); } // simple update to be received private void ECLDirectAllOps() { String tn = "ECLDirectAllOps"; debugInfo(tn, "Starting test\n\n"); try { LDIFWriter ldifWriter = getLDIFWriter(); // Creates broker on o=test ReplicationBroker server01 = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING), (short) 1201, 100, replicationServerPort, 1000, true); int ts = 1; String user1entryUUID = "11111111-1111-1111-1111-111111111111"; String baseUUID = "22222222-2222-2222-2222-222222222222"; // Publish DEL ChangeNumber cn1 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201); DeleteMsg delMsg = new DeleteMsg("uid="+tn+"1," + TEST_ROOT_DN_STRING, cn1, tn+"uuid1"); server01.publish(delMsg); debugInfo(tn, " publishes " + delMsg.getChangeNumber()); // Publish ADD ChangeNumber cn2 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201); String lentry = new String("dn: uid="+tn+"2," + TEST_ROOT_DN_STRING + "\n" + "objectClass: top\n" + "objectClass: domain\n" + "entryUUID: "+user1entryUUID+"\n"); Entry entry = TestCaseUtils.entryFromLdifString(lentry); AddMsg addMsg = new AddMsg( cn2, "uid="+tn+"2," + TEST_ROOT_DN_STRING, user1entryUUID, baseUUID, entry.getObjectClassAttribute(), entry.getAttributes(), new ArrayList<Attribute>()); server01.publish(addMsg); debugInfo(tn, " publishes " + addMsg.getChangeNumber()); // Publish MOD ChangeNumber cn3 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201); Attribute attr1 = Attributes.create("description", "new value"); Modification mod1 = new Modification(ModificationType.REPLACE, attr1); List<Modification> mods = new ArrayList<Modification>(); mods.add(mod1); ModifyMsg modMsg = new ModifyMsg(cn3, DN .decode("uid="+tn+"3," + TEST_ROOT_DN_STRING), mods, tn+"uuid3"); server01.publish(modMsg); debugInfo(tn, " publishes " + modMsg.getChangeNumber()); // Publish modDN ChangeNumber cn4 = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201); ModifyDNOperationBasis op = new ModifyDNOperationBasis(connection, 1, 1, null, DN.decode("uid="+tn+"4," + TEST_ROOT_DN_STRING), // entryDN RDN.decode("uid="+tn+"new4"), // new rdn true, // deleteoldrdn DN.decode(TEST_ROOT_DN_STRING2)); // new superior op.setAttachment(SYNCHROCONTEXT, new ModifyDnContext(cn4, tn+"uuid4", "newparentId")); LocalBackendModifyDNOperation localOp = new LocalBackendModifyDNOperation(op); ModifyDNMsg modDNMsg = new ModifyDNMsg(localOp); server01.publish(modDNMsg); debugInfo(tn, " publishes " + modDNMsg.getChangeNumber()); sleep(600); String cookie= ""; // search on 'cn=changelog' debugInfo(tn, "STEP 1 - from empty cookie("+cookie+")"); LinkedHashSet<String> attributes = new LinkedHashSet<String>(); attributes.add("+"); attributes.add("*"); ExternalChangelogRequestControl control = new ExternalChangelogRequestControl(true, new MultiDomainServerState()); ArrayList<Control> controls = new ArrayList<Control>(0); controls.add(control); InternalSearchOperation searchOp = connection.processSearch( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, 0, // Size limit 0, // Time limit false, // Types only LDAPFilter.decode("(targetDN=*"+tn+"*,o=test)"), attributes, controls, null); sleep(500); // test success assertEquals(searchOp.getResultCode(), ResultCode.SUCCESS, searchOp.getErrorMessage().toString()); // test 4 entries returned LinkedList<SearchResultEntry> entries = searchOp.getSearchEntries(); // 4 entries expected assertEquals(searchOp.getSearchEntries().size(), 4); if (entries != null) { int i=0; for (SearchResultEntry resultEntry : entries) { i++; debugInfo(tn, "Result entry returned:" + resultEntry.toLDIFString()); ldifWriter.writeEntry(resultEntry); if (i==1) { // check the DEL entry has the right content checkValue(resultEntry,"replicationcsn",cn1.toString()); checkValue(resultEntry,"replicaidentifier","1201"); checkValue(resultEntry,"targetdn","uid="+tn+"1," + TEST_ROOT_DN_STRING); checkValue(resultEntry,"changetype","delete"); checkValue(resultEntry,"cookie","o=test:"+cn1.toString()+";"); checkValue(resultEntry,"targetentryuuid",tn+"uuid1"); } else if (i==2) { // check the ADD entry has the right content String expectedValue1 = "objectClass: domain\nobjectClass: top\n" + "entryUUID: 11111111-1111-1111-1111-111111111111\n\n"; String expectedValue2 = "entryUUID: 11111111-1111-1111-1111-111111111111\n" + "objectClass: domain\nobjectClass: top\n\n"; checkPossibleValues(resultEntry,"changes",expectedValue1, expectedValue2); checkValue(resultEntry,"replicationcsn",cn2.toString()); checkValue(resultEntry,"replicaidentifier","1201"); checkValue(resultEntry,"targetdn","uid="+tn+"2," + TEST_ROOT_DN_STRING); checkValue(resultEntry,"changetype","add"); checkValue(resultEntry,"cookie","o=test:"+cn2.toString()+";"); checkValue(resultEntry,"targetentryuuid",user1entryUUID); } else if (i==3) { // check the MOD entry has the right content String expectedValue = "replace: description\n" + "description: new value\n-\n"; checkValue(resultEntry,"changes",expectedValue); checkValue(resultEntry,"replicationcsn",cn3.toString()); checkValue(resultEntry,"replicaidentifier","1201"); checkValue(resultEntry,"targetdn","uid="+tn+"3," + TEST_ROOT_DN_STRING); checkValue(resultEntry,"changetype","modify"); checkValue(resultEntry,"cookie","o=test:"+cn3.toString()+";"); checkValue(resultEntry,"targetentryuuid",tn+"uuid3"); } else if (i==4) { // check the MODDN entry has the right content checkValue(resultEntry,"replicationcsn",cn4.toString()); checkValue(resultEntry,"replicaidentifier","1201"); checkValue(resultEntry,"targetdn","uid="+tn+"4," + TEST_ROOT_DN_STRING); checkValue(resultEntry,"changetype","modrdn"); checkValue(resultEntry,"cookie","o=test:"+cn4.toString()+";"); checkValue(resultEntry,"targetentryuuid",tn+"uuid4"); checkValue(resultEntry,"newrdn","uid=ECLDirectAllOpsnew4"); checkValue(resultEntry,"newsuperior",TEST_ROOT_DN_STRING2); checkValue(resultEntry,"deleteoldrdn","true"); } } } server01.stop(); } catch(Exception e) { debugInfo(tn, "Ending test with exception:\n" + stackTraceToSingleLineString(e)); assertTrue(e == null); } debugInfo(tn, "Ending test with success"); } private static void checkValue(Entry entry, String attrName, String expectedValue) { AttributeValue av = null; try { List<Attribute> attrs = entry.getAttribute(attrName); Attribute a = attrs.iterator().next(); av = a.iterator().next(); String encodedValue = av.toString(); assertTrue(encodedValue.equalsIgnoreCase(expectedValue), "In entry " + entry + " attr <" + attrName + "> equals " + av + " instead of expected value " + expectedValue); } catch(Exception e) { assertTrue(false, "In entry " + entry + " attr <" + attrName + "> equals " + av + " instead of expected value " + expectedValue); } } private static void checkPossibleValues(Entry entry, String attrName, String expectedValue1, String expectedValue2) { AttributeValue av = null; try { List<Attribute> attrs = entry.getAttribute(attrName); Attribute a = attrs.iterator().next(); av = a.iterator().next(); String encodedValue = av.toString(); assertTrue( (encodedValue.equalsIgnoreCase(expectedValue1) || encodedValue.equalsIgnoreCase(expectedValue2)), "In entry " + entry + " attr <" + attrName + "> equals " + av + " instead of one of the expected values " + expectedValue1 + " or " + expectedValue2); } catch(Exception e) { assertTrue(false, "In entry " + entry + " attr <" + attrName + "> equals " + av + " instead of one of the expected values " + expectedValue1 + " or " + expectedValue2); } } /** * Test persistent search */ private void ECLDirectPsearch(boolean changesOnly) { String tn = "ECLDirectPsearch_" + String.valueOf(changesOnly); debugInfo(tn, "Starting test \n\n"); Socket s =null; // create stats for (ConnectionHandler ch : DirectoryServer.getConnectionHandlers()) { if (ch instanceof LDAPConnectionHandler) { LDAPConnectionHandler lch = (LDAPConnectionHandler) ch; if (!lch.useSSL()) { ldapStatistics = lch.getStatTracker(); } } } assertNotNull(ldapStatistics); try { // Create broker on suffix ReplicationBroker server01 = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING), (short) 1201, 100, replicationServerPort, 1000, true); int ts = 1; // Produce update on this suffix ChangeNumber cn = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201); DeleteMsg delMsg = new DeleteMsg("uid=" + tn + "1," + TEST_ROOT_DN_STRING, cn, tn+"uuid1"); debugInfo(tn, " publishing " + delMsg.getChangeNumber()); server01.publish(delMsg); this.sleep(500); // let's be sure the message is in the RS // Creates cookie control ArrayList<Control> controls = getControls(""); // Creates psearch control HashSet<PersistentSearchChangeType> changeTypes = new HashSet<PersistentSearchChangeType>(); changeTypes.add(PersistentSearchChangeType.ADD); changeTypes.add(PersistentSearchChangeType.DELETE); changeTypes.add(PersistentSearchChangeType.MODIFY); changeTypes.add(PersistentSearchChangeType.MODIFY_DN); boolean returnECs = true; PersistentSearchControl persSearchControl = new PersistentSearchControl( changeTypes, changesOnly, returnECs); controls.add(persSearchControl); // Creates request SearchRequestProtocolOp searchRequest = new SearchRequestProtocolOp( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, Integer.MAX_VALUE, Integer.MAX_VALUE, false, LDAPFilter.decode("(targetDN=*"+tn+"*,o=test)"), null); // Connects and bind s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort()); org.opends.server.tools.LDAPReader r = new org.opends.server.tools.LDAPReader(s); LDAPWriter w = new LDAPWriter(s); s.setSoTimeout(1500000); bindAsManager(w, r); // Since we are going to be watching the post-response count, we need to // wait for the server to become idle before kicking off the next request // to ensure that any remaining post-response processing from the previous // operation has completed. assertTrue(DirectoryServer.getWorkQueue().waitUntilIdle(10000)); InvocationCounterPlugin.resetAllCounters(); long searchRequests = ldapStatistics.getSearchRequests(); long searchEntries = ldapStatistics.getSearchResultEntries(); long searchReferences = ldapStatistics.getSearchResultReferences(); long searchesDone = ldapStatistics.getSearchResultsDone(); debugInfo(tn, "Sending the PSearch request filter=(targetDN=*"+tn+"*,o=test)"); LDAPMessage message; message = new LDAPMessage(2, searchRequest, controls); w.writeMessage(message); this.sleep(500); SearchResultEntryProtocolOp searchResultEntry = null; SearchResultDoneProtocolOp searchResultDone = null; if (changesOnly == false) { // Wait for change 1 debugInfo(tn, "Waiting for : INIT search expected to return change 1"); searchEntries = 0; message = null; try { while ((searchEntries<1) && (message = r.readMessage()) != null) { debugInfo(tn, "First search returns " + message.getProtocolOpType() + message + " " + searchEntries); switch (message.getProtocolOpType()) { case LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY: searchResultEntry = message.getSearchResultEntryProtocolOp(); searchEntries++; break; case LDAPConstants.OP_TYPE_SEARCH_RESULT_REFERENCE: searchReferences++; break; case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE: searchResultDone = message.getSearchResultDoneProtocolOp(); assertEquals( searchResultDone.getResultCode(), ResultCode.SUCCESS, searchResultDone.getErrorMessage().toString()); // assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1); searchesDone++; break; } } } catch(Exception e) { debugInfo(tn, "INIT search failed with e=" + stackTraceToSingleLineString(e)); } debugInfo(tn, "INIT search done with success. searchEntries=" + searchEntries + " searchesDone="+ searchesDone); } // Produces change 2 cn = new ChangeNumber(TimeThread.getTime(), ts++, (short)1201); String expectedDn = "uid=" + tn + "2," + TEST_ROOT_DN_STRING; delMsg = new DeleteMsg(expectedDn, cn, tn + "uuid2"); debugInfo(tn, " publishing " + delMsg.getChangeNumber()); server01.publish(delMsg); this.sleep(1000); debugInfo(tn, delMsg.getChangeNumber() + " published , will wait for new entries (Persist)"); // wait for the 1 new entry searchEntries = 0; searchResultEntry = null; searchResultDone = null; message = null; while ((searchEntries<1) && (message = r.readMessage()) != null) { debugInfo(tn, "2nd search returns " + message.getProtocolOpType() + message); switch (message.getProtocolOpType()) { case LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY: searchResultEntry = message.getSearchResultEntryProtocolOp(); searchEntries++; break; case LDAPConstants.OP_TYPE_SEARCH_RESULT_REFERENCE: searchReferences++; break; case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE: searchResultDone = message.getSearchResultDoneProtocolOp(); assertEquals( searchResultDone.getResultCode(), ResultCode.SUCCESS, searchResultDone.getErrorMessage().toString()); // assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1); searchesDone++; break; } } sleep(1000); // Check we received change 2 for (LDAPAttribute a : searchResultEntry.getAttributes()) { if (a.getAttributeType().equalsIgnoreCase("targetDN")) { for (ByteString av : a.getValues()) { assertTrue(av.toString().equalsIgnoreCase(expectedDn), "Entry returned by psearch is " + av.toString() + " when expected is " + expectedDn); } } } debugInfo(tn, "Second search done successfully : " + searchResultEntry); server01.stop(); try { s.close(); } catch (Exception e) {}; sleep(1000); // TODO: Testing ACI is disabled because it is currently failing when // ran in the precommit target while it works well when running alone. // anonymous search returns entries from cn=changelog whereas it // should not. Probably a previous test in the nightlytests suite is // removing/modifying some ACIs... // When problem found, we have to re-enable this test. if (false) { // ACI step debugInfo(tn, "Starting ACI step"); s = new Socket("127.0.0.1", TestCaseUtils.getServerLdapPort()); r = new org.opends.server.tools.LDAPReader(s); w = new LDAPWriter(s); s.setSoTimeout(1500000); bindAsWhoEver(w, r, "toto", "tutu", LDAPResultCode.OPERATIONS_ERROR); searchRequest = new SearchRequestProtocolOp( ByteString.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE, DereferencePolicy.NEVER_DEREF_ALIASES, Integer.MAX_VALUE, Integer.MAX_VALUE, false, LDAPFilter.decode("(targetDN=*directpsearch*,o=test)"), null); debugInfo(tn, "ACI test : sending search"); message = new LDAPMessage(2, searchRequest, getControls("")); w.writeMessage(message); searchesDone=0; searchEntries = 0; searchResultEntry = null; searchResultDone = null; while ((searchesDone==0) && (message = r.readMessage()) != null) { debugInfo(tn, "ACI test : message returned " + message.getProtocolOpType() + message); switch (message.getProtocolOpType()) { case LDAPConstants.OP_TYPE_SEARCH_RESULT_ENTRY: searchResultEntry = message.getSearchResultEntryProtocolOp(); //assertTrue(false, "Unexpected entry returned in ACI test of " + tn + searchResultEntry); searchEntries++; break; case LDAPConstants.OP_TYPE_SEARCH_RESULT_REFERENCE: searchReferences++; break; case LDAPConstants.OP_TYPE_SEARCH_RESULT_DONE: searchResultDone = message.getSearchResultDoneProtocolOp(); assertEquals(searchResultDone.getResultCode(), ResultCode.SUCCESS.getIntValue()); // assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1); searchesDone++; break; } } // search should end with success assertTrue(searchesDone==1); // but returning no entry assertEquals(searchEntries,0, "Bad search entry# in ACI test of " + tn); } try { s.close(); } catch (Exception e) {}; sleep(1000); } catch(Exception e) { assertTrue(e==null, stackTraceToSingleLineString(e)); } debugInfo(tn, "Ends test successfuly"); } // utility - bind as required private void bindAsManager(LDAPWriter w, org.opends.server.tools.LDAPReader r) throws IOException, LDAPException, ASN1Exception, InterruptedException { bindAsWhoEver(w, r, "cn=Directory Manager", "password", LDAPResultCode.SUCCESS); } // utility - bind as required private void bindAsWhoEver(LDAPWriter w, org.opends.server.tools.LDAPReader r, String bindDN, String password, int expected) throws IOException, LDAPException, ASN1Exception, InterruptedException { // Since we are going to be watching the post-response count, we need to // wait for the server to become idle before kicking off the next request to // ensure that any remaining post-response processing from the previous // operation has completed. assertTrue(DirectoryServer.getWorkQueue().waitUntilIdle(10000)); InvocationCounterPlugin.resetAllCounters(); BindRequestProtocolOp bindRequest = new BindRequestProtocolOp( ByteString.valueOf(bindDN), 3, ByteString.valueOf(password)); LDAPMessage message = new LDAPMessage(1, bindRequest); w.writeMessage(message); message = r.readMessage(); BindResponseProtocolOp bindResponse = message.getBindResponseProtocolOp(); // assertEquals(InvocationCounterPlugin.waitForPostResponse(), 1); assertEquals(bindResponse.getResultCode(), expected); } /** * Clean up the environment. * * @throws Exception If the environment could not be set up. */ @AfterClass public void classCleanUp() throws Exception { callParanoiaCheck = false; super.classCleanUp(); shutdown(); paranoiaCheck(); } /** * After the tests stop the replicationServer. */ protected void shutdown() throws Exception { if (replicationServer != null) replicationServer.remove(); /* TestCaseUtils.dsconfig( "delete-replication-server", "--provider-name", "Multimaster Synchronization"); */ replicationServer = null; } /** * Utility - sleeping as long as required */ private void sleep(long time) { try { Thread.sleep(time); } catch (InterruptedException ex) { fail("Error sleeping " + ex.getMessage()); } } /** * Utility - log debug message - highlight it is from the test and not * from the server code. Makes easier to observe the test steps. */ private void debugInfo(String tn, String s) { //if (debugEnabled()) { logError(Message.raw(Category.SYNC, Severity.NOTICE, "** TEST " + tn + " ** " + s)); TRACER.debugInfo("** TEST " + tn + " ** " + s); } } /** * Utility - create a second backend in order to test ECL with 2 suffixes. */ private static Backend initializeTestBackend2(boolean createBaseEntry) throws IOException, InitializationException, ConfigException, DirectoryException { DN baseDN = DN.decode(TEST_ROOT_DN_STRING2); // Retrieve backend. Warning: it is important to perform this each time, // because a test may have disabled then enabled the backend (i.e a test // performing an import task). As it is a memory backend, when the backend // is re-enabled, a new backend object is in fact created and old reference // to memory backend must be invalidated. So to prevent this problem, we // retrieve the memory backend reference each time before cleaning it. MemoryBackend memoryBackend = (MemoryBackend)DirectoryServer.getBackend(TEST_BACKEND_ID2); if (memoryBackend == null) { memoryBackend = new MemoryBackend(); memoryBackend.setBackendID(TEST_BACKEND_ID2); memoryBackend.setBaseDNs(new DN[] {baseDN}); memoryBackend.initializeBackend(); DirectoryServer.registerBackend(memoryBackend); } memoryBackend.clearMemoryBackend(); if (createBaseEntry) { Entry e = createEntry(baseDN); memoryBackend.addEntry(e, null); } return memoryBackend; } private static void removeTestBackend2(Backend backend) { MemoryBackend memoryBackend = (MemoryBackend)backend; memoryBackend.finalizeBackend(); DirectoryServer.deregisterBackend(memoryBackend); } } opends/tests/unit-tests-testng/src/server/org/opends/server/replication/protocol/SynchronizationMsgTest.java
@@ -22,20 +22,26 @@ * CDDL HEADER END * * * Copyright 2006-2008 Sun Microsystems, Inc. * Copyright 2006-2009 Sun Microsystems, Inc. */ package org.opends.server.replication.protocol; import static org.opends.server.replication.protocol.OperationContext.*; import static org.testng.Assert.*; import static org.opends.server.TestCaseUtils.TEST_ROOT_DN_STRING; import static org.opends.server.replication.protocol.OperationContext.SYNCHROCONTEXT; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertFalse; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; import java.util.Iterator; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.UUID; import java.util.zip.DataFormatException; import org.opends.messages.Message; import org.opends.server.core.AddOperation; import org.opends.server.core.AddOperationBasis; import org.opends.server.core.DeleteOperationBasis; @@ -44,8 +50,13 @@ import org.opends.server.core.ModifyOperationBasis; import org.opends.server.protocols.internal.InternalClientConnection; import org.opends.server.replication.ReplicationTestCase; import org.opends.server.replication.common.AssuredMode; import org.opends.server.replication.common.ChangeNumber; import org.opends.server.replication.common.DSInfo; import org.opends.server.replication.common.MultiDomainServerState; import org.opends.server.replication.common.RSInfo; import org.opends.server.replication.common.ServerState; import org.opends.server.replication.common.ServerStatus; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeBuilder; import org.opends.server.types.AttributeType; @@ -60,15 +71,9 @@ import org.opends.server.workflowelement.localbackend.LocalBackendAddOperation; import org.opends.server.workflowelement.localbackend.LocalBackendDeleteOperation; import org.opends.server.workflowelement.localbackend.LocalBackendModifyDNOperation; 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.RSInfo; import org.opends.server.replication.common.ServerStatus; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import static org.opends.server.TestCaseUtils.*; /** * Test the constructors, encoders and decoders of the replication protocol @@ -563,18 +568,18 @@ throws Exception { AckMsg msg1, msg2 ; // Consctructor test (with ChangeNumber) // Chech that retrieved CN is OK msg1 = new AckMsg(cn); assertEquals(msg1.getChangeNumber().compareTo(cn), 0); // Check default values for error info assertFalse(msg1.hasTimeout()); assertFalse(msg1.hasWrongStatus()); assertFalse(msg1.hasReplayError()); assertTrue(msg1.getFailedServers().size() == 0); // Check constructor with error info msg1 = new AckMsg(cn, hasTimeout, hasWrongStatus, hasReplayError, failedServers); assertEquals(msg1.getChangeNumber().compareTo(cn), 0); @@ -582,7 +587,7 @@ assertTrue(msg1.hasWrongStatus() == hasWrongStatus); assertTrue(msg1.hasReplayError() == hasReplayError); assertEquals(msg1.getFailedServers(), failedServers); // Consctructor test (with byte[]) msg2 = new AckMsg(msg1.getBytes()); assertEquals(msg2.getChangeNumber().compareTo(cn), 0); @@ -590,7 +595,7 @@ assertTrue(msg1.hasWrongStatus() == msg2.hasWrongStatus()); assertTrue(msg1.hasReplayError() == msg2.hasReplayError()); assertEquals(msg1.getFailedServers(), msg2.getFailedServers()); // Check invalid bytes for constructor byte[] b = msg1.getBytes(); b[0] = ReplicationMsg.MSG_TYPE_ADD; @@ -604,11 +609,53 @@ { assertTrue(true); } // Check that retrieved CN is OK msg2 = (AckMsg) ReplicationMsg.generateMsg(msg1.getBytes()); } @Test() public void eclUpdateMsg() throws Exception { // create a msg to put in the eclupdatemsg InternalClientConnection connection = InternalClientConnection.getRootConnection(); DeleteOperationBasis opBasis = new DeleteOperationBasis(connection, 1, 1,null, DN.decode("cn=t1")); LocalBackendDeleteOperation op = new LocalBackendDeleteOperation(opBasis); ChangeNumber cn = new ChangeNumber(TimeThread.getTime(), (short) 123, (short) 45); op.setAttachment(SYNCHROCONTEXT, new DeleteContext(cn, "uniqueid")); DeleteMsg delmsg = new DeleteMsg(op); String serviceId = "serviceid"; // create a cookie MultiDomainServerState cookie = new MultiDomainServerState( "o=test:000001210b6f21e904b100000001 000001210b6f21e904b200000001;" + "o=test2:000001210b6f21e904b100000002 000001210b6f21e904b200000002;"); // Constructor test ECLUpdateMsg msg1 = new ECLUpdateMsg(delmsg, cookie, serviceId); assertTrue(msg1.getCookie().equalsTo(cookie)); assertTrue(msg1.getServiceId().equalsIgnoreCase(serviceId)); DeleteMsg delmsg2 = (DeleteMsg)msg1.getUpdateMsg(); assertTrue(delmsg.compareTo(delmsg2)==0); // Consctructor test (with byte[]) ECLUpdateMsg msg2 = new ECLUpdateMsg(msg1.getBytes()); assertTrue(msg2.getCookie().equalsTo(msg2.getCookie())); assertTrue(msg2.getCookie().equalsTo(cookie)); assertTrue(msg2.getServiceId().equalsIgnoreCase(msg1.getServiceId())); assertTrue(msg2.getServiceId().equalsIgnoreCase(serviceId)); DeleteMsg delmsg1 = (DeleteMsg)msg1.getUpdateMsg(); delmsg2 = (DeleteMsg)msg2.getUpdateMsg(); assertTrue(delmsg2.compareTo(delmsg)==0); assertTrue(delmsg2.compareTo(delmsg1)==0); } @DataProvider(name="createServerStartData") public Object [][] createServerStartData() throws Exception { @@ -1120,4 +1167,85 @@ UpdateMsg newMsg = new UpdateMsg(msg.getBytes()); assertEquals(test.getBytes(), newMsg.getPayload()); } /** * Test that ServerStartMsg encoding and decoding works * by checking that : msg == new ServerStartMsg(msg.getBytes()). */ @Test(dataProvider="createServerStartData") public void startECLMsgTest(short serverId, String baseDN, int window, ServerState state, long genId, boolean sslEncryption, byte groupId) throws Exception { ServerStartECLMsg msg = new ServerStartECLMsg(baseDN, window, window, window, window, window, window, state, ProtocolVersion.getCurrentVersion(), genId, sslEncryption, groupId); ServerStartECLMsg newMsg = new ServerStartECLMsg(msg.getBytes()); assertEquals(msg.getMaxReceiveDelay(), newMsg.getMaxReceiveDelay()); assertEquals(msg.getMaxReceiveQueue(), newMsg.getMaxReceiveQueue()); assertEquals(msg.getMaxSendDelay(), newMsg.getMaxSendDelay()); assertEquals(msg.getMaxSendQueue(), newMsg.getMaxSendQueue()); assertEquals(msg.getWindowSize(), newMsg.getWindowSize()); assertEquals(msg.getHeartbeatInterval(), newMsg.getHeartbeatInterval()); assertEquals(msg.getSSLEncryption(), newMsg.getSSLEncryption()); assertEquals(msg.getServerState().getMaxChangeNumber((short)1), newMsg.getServerState().getMaxChangeNumber((short)1)); assertEquals(msg.getVersion(), newMsg.getVersion()); assertEquals(msg.getGenerationId(), newMsg.getGenerationId()); assertTrue(msg.getGroupId() == newMsg.getGroupId()); } /** * Test StartSessionMsg encoding and decoding. */ @Test() public void startECLSessionMsgTest() throws Exception { // data ChangeNumber changeNumber = new ChangeNumber(TimeThread.getTime(), (short) 123, (short) 45); String generalizedState = new String("fakegenstate"); ServerState state = new ServerState(); assertTrue(state.update(new ChangeNumber((long)75, 5,(short)263))); short mode = 3; int firstDraftChangeNumber = 13; int lastDraftChangeNumber = 14; String myopid = new String("fakeopid"); // create original StartECLSessionMsg msg = new StartECLSessionMsg(); msg.setChangeNumber(changeNumber); msg.setCrossDomainServerState(generalizedState); msg.setPersistent(StartECLSessionMsg.PERSISTENT); msg.setFirstDraftChangeNumber(firstDraftChangeNumber); msg.setLastDraftChangeNumber(lastDraftChangeNumber); msg.setECLRequestType(mode); msg.setOperationId(myopid); ArrayList<String> dns = new ArrayList<String>(); String dn1 = "cn=admin data"; String dn2 = "cn=config"; dns.add(dn1); dns.add(dn2); msg.setExcludedDNs(dns); // create copy StartECLSessionMsg newMsg = new StartECLSessionMsg(msg.getBytes()); // test equality between the two copies assertEquals(msg.getChangeNumber(), newMsg.getChangeNumber()); assertTrue(msg.isPersistent() == newMsg.isPersistent()); assertTrue(msg.getFirstDraftChangeNumber() == newMsg.getFirstDraftChangeNumber()); assertEquals(msg.getECLRequestType(), newMsg.getECLRequestType()); assertEquals(msg.getLastDraftChangeNumber(), newMsg.getLastDraftChangeNumber()); assertTrue( msg.getCrossDomainServerState().equalsIgnoreCase(newMsg.getCrossDomainServerState())); assertTrue( msg.getOperationId().equalsIgnoreCase(newMsg.getOperationId())); ArrayList<String> dns2 = newMsg.getExcludedServiceIDs(); assertTrue(dns2.size()==2); boolean dn1found=false,dn2found=false; for (String dn : dns2) { if (!dn1found) dn1found=(dn.compareTo(dn1)==0); if (!dn2found) dn2found=(dn.compareTo(dn2)==0); } assertTrue(dn1found); assertTrue(dn2found); } } opends/tests/unit-tests-testng/src/server/org/opends/server/replication/server/ReplicationServerTest.java
@@ -90,6 +90,8 @@ import org.opends.server.util.LDIFWriter; import org.opends.server.util.TimeThread; import static org.opends.server.util.ServerConstants.OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE; import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString; import org.opends.server.workflowelement.localbackend.LocalBackendModifyDNOperation; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; @@ -175,7 +177,7 @@ private void debugInfo(String s) { ErrorLogger.logError(Message.raw(Category.SYNC, Severity.NOTICE, "** TEST ** " + s)); //ErrorLogger.logError(Message.raw(Category.SYNC, Severity.NOTICE, "** TEST ** " + s)); if (debugEnabled()) { TRACER.debugInfo("** TEST ** " + s); @@ -689,7 +691,7 @@ new ChangeNumberGenerator(serverId , (long) 0); broker[i] = openReplicationSession( DN.decode(TEST_ROOT_DN_STRING), serverId, 100, replicationServerPort, 100000, 1000, 0, false); 100, replicationServerPort, 3000, 1000, 0, true); assertTrue(broker[i].isConnected()); @@ -706,10 +708,11 @@ { reader[i].start(); } debugInfo("Ending multipleWriterMultipleReader"); debugInfo("multipleWriterMultipleReader produces and readers started"); } finally { debugInfo("multipleWriterMultipleReader wait producers"); for (int i = 0; i< THREADS; i++) { if (producer[i] != null) @@ -717,6 +720,7 @@ // kill the thread in case it is not yet stopped. producer[i].interrupt(); } debugInfo("multipleWriterMultipleReader producers done, wait readers"); for (int i = 0; i< THREADS; i++) { if (reader[i] != null) @@ -724,15 +728,18 @@ // kill the thread in case it is not yet stopped. reader[i].interrupt(); } debugInfo("multipleWriterMultipleReader reader's done"); for (int i = 0; i< THREADS; i++) { if (broker[i] != null) broker[i].stop(); } debugInfo("multipleWriterMultipleReader brokers stopped"); replicationServer.clearDb(); TestCaseUtils.initializeTestBackend(true); } debugInfo("Ending multipleWriterMultipleReader"); } @@ -1123,6 +1130,8 @@ */ private class BrokerReader extends Thread { int count; private ReplicationBroker broker; private int numMsgRcv = 0; private final int numMsgExpected; @@ -1142,8 +1151,11 @@ @Override public void run() { debugInfo("BrokerReader " + broker.getServerId() + " starts"); // loop receiving messages until either we get a timeout // because there is nothing left or an error condition happens. count = 0; try { while (true) @@ -1165,7 +1177,8 @@ } catch (Exception e) { assertTrue(false, "a BrokerReader received an Exception" + e.getMessage()); "a BrokerReader received an Exception" + e.getMessage() + stackTraceToSingleLineString(e)); } } } @@ -1194,6 +1207,8 @@ @Override public void run() { debugInfo("BrokerWriter " + broker.getServerId() + " starts"); int ccount = count; /* * Simple loop creating changes and sending them * to the replicationServer. @@ -1206,7 +1221,11 @@ new DeleteMsg("o=example," + TEST_ROOT_DN_STRING, gen.newChangeNumber(), "uid"); broker.publish(msg); if ((count % 10) == 0) debugInfo("BrokerWriter " + broker.getServerId() + " sent="+count); } debugInfo("BrokerWriter " + broker.getServerId() + " ends sent="+ccount); } }