major refactoring introducing tagging the replication streams with the Replicable IDs and introducing a new URL parameter for the initial load servlet request

This commit is contained in:
Axel Uhl committed 2014-11-13 23:06:05 +01:00
1 parent 401da5f50b
commit bb5a16e1ac
28 files changed
+648 -370

No files matched your search

@@ -17,6 +17,7 @@ Require-Bundle: com.sap.sailing.domain,
org.objenesis;bundle-version="1.3.0",
org.junit4;bundle-version="4.8.2",
org.json.simple;bundle-version="1.1.0",
com.sap.sse.common
com.sap.sse.common,
com.sap.sse.replication
Export-Package: com.sap.sailing.domain.test,
com.sap.sailing.domain.test.mock
@@ -34,4 +34,5 @@ Require-Bundle: com.googlecode.java-diff-utils;bundle-version="1.3.0",
com.sap.sailing.geocoding,
com.sap.sse.datamining.shared;bundle-version="1.0.0",
com.sap.sse.common,
com.sap.sse
com.sap.sse,
com.sap.sse.replication
@@ -22,6 +22,7 @@ import com.sap.sailing.domain.tracking.MarkPassing;
import com.sap.sailing.domain.tracking.TrackedRace;
import com.sap.sailing.domain.tracking.TrackedRegattaRegistry;
import com.sap.sse.common.TimePoint;
import com.sap.sse.replication.impl.ObjectInputStreamResolvingAgainstCache;
public interface DomainFactory extends SharedDomainFactory {
/**
@@ -38,7 +39,7 @@ public interface DomainFactory extends SharedDomainFactory {
* de-serialized instances need to be replaced by / resolved to the counterparts already known by this factory.
* The stream returned by this method can be used
*/
ObjectInputStreamResolvingAgainstDomainFactory createObjectInputStreamResolvingAgainstThisFactory(InputStream inputStream) throws IOException;
ObjectInputStreamResolvingAgainstCache<DomainFactory> createObjectInputStreamResolvingAgainstThisFactory(InputStream inputStream) throws IOException;
ScoringScheme createScoringScheme(ScoringSchemeType scoringSchemeType);
@@ -1,62 +0,0 @@
package com.sap.sailing.domain.base;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectStreamClass;
import com.sap.sse.common.IsManagedByCache;
/**
* During de-serialization, resolves objects managed by a {@link DomainFactory} using the domain factory instance passed
* to the constructor. Additionally, class loading is managed using the current thread's context class loader which is
* required in and works well for OSGi environments.
* <p>
*
* Create an instance of this class using
* {@link DomainFactory#createObjectInputStreamResolvingAgainstThisFactory(InputStream)}.
*
* @author Axel Uhl (D043530)
*
*/
public abstract class ObjectInputStreamResolvingAgainstDomainFactory extends ObjectInputStream implements HasDomainFactory {
private final DomainFactory domainFactory;
/**
* Package protected on purpose; instances to be created using
* {@link DomainFactory#createObjectInputStreamResolvingAgainstThisFactory(InputStream)}. This constructor
* {@link #enableResolveObject(boolean) enables resolving} by default.
*/
protected ObjectInputStreamResolvingAgainstDomainFactory(InputStream in, DomainFactory domainFactory) throws IOException {
super(in);
this.domainFactory = domainFactory;
enableResolveObject(true);
}
@Override
public DomainFactory getDomainFactory() {
return domainFactory;
}
@Override
protected Class<?> resolveClass(ObjectStreamClass classDesc) throws IOException, ClassNotFoundException {
String className = classDesc.getName();
// see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6554519
// If using loadClass(...) on the class loader directly, an exception is thrown:
// StreamCorruptedException: invalid type code 00
return Class.forName(className, /* initialize */ true, Thread.currentThread().getContextClassLoader());
}
@Override
protected Object resolveObject(Object o) {
Object result;
if (o instanceof IsManagedByCache) {
@SuppressWarnings("unchecked")
IsManagedByCache<SharedDomainFactory> castResult = ((IsManagedByCache<SharedDomainFactory>) o).resolve(getDomainFactory());
result = castResult;
} else {
result = o;
}
return result;
}
}
@@ -13,7 +13,6 @@ import com.sap.sailing.domain.base.CompetitorStore;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.Fleet;
import com.sap.sailing.domain.base.Mark;
import com.sap.sailing.domain.base.ObjectInputStreamResolvingAgainstDomainFactory;
import com.sap.sailing.domain.base.RaceColumn;
import com.sap.sailing.domain.base.RaceDefinition;
import com.sap.sailing.domain.base.Waypoint;
@@ -61,6 +60,7 @@ import com.sap.sailing.geocoding.ReverseGeocoder;
import com.sap.sse.common.TimePoint;
import com.sap.sse.common.Util;
import com.sap.sse.common.impl.MillisecondsTimePoint;
import com.sap.sse.replication.impl.ObjectInputStreamResolvingAgainstCache;
public class DomainFactoryImpl extends SharedDomainFactoryImpl implements DomainFactory {
private static Logger logger = Logger.getLogger(DomainFactoryImpl.class.getName());
@@ -82,7 +82,7 @@ public class DomainFactoryImpl extends SharedDomainFactoryImpl implements Domain
}
@Override
public ObjectInputStreamResolvingAgainstDomainFactory createObjectInputStreamResolvingAgainstThisFactory(InputStream inputStream) throws IOException {
public ObjectInputStreamResolvingAgainstCache<DomainFactory> createObjectInputStreamResolvingAgainstThisFactory(InputStream inputStream) throws IOException {
return new ObjectInputStreamResolvingAgainstDomainFactoryImpl(inputStream, this);
}
@@ -4,9 +4,9 @@ import java.io.IOException;
import java.io.InputStream;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.ObjectInputStreamResolvingAgainstDomainFactory;
import com.sap.sse.replication.impl.ObjectInputStreamResolvingAgainstCache;
public class ObjectInputStreamResolvingAgainstDomainFactoryImpl extends ObjectInputStreamResolvingAgainstDomainFactory {
public class ObjectInputStreamResolvingAgainstDomainFactoryImpl extends ObjectInputStreamResolvingAgainstCache<DomainFactory> {
ObjectInputStreamResolvingAgainstDomainFactoryImpl(InputStream in, DomainFactory domainFactory)
throws IOException {
super(in, domainFactory);
@@ -1,12 +1,12 @@
package com.sap.sailing.gwt.ui.test;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.net.UnknownHostException;
import java.text.ParseException;
import java.util.Collections;
import java.util.List;
import javax.xml.bind.JAXBException;
@@ -33,11 +33,11 @@ import com.sap.sailing.domain.tracking.TrackerManager;
import com.sap.sailing.domain.tractracadapter.TracTracAdapterFactory;
import com.sap.sailing.gwt.ui.server.SailingServiceImpl;
import com.sap.sailing.server.RacingEventService;
import com.sap.sailing.server.RacingEventServiceOperation;
import com.sap.sailing.server.impl.RacingEventServiceImpl;
import com.sap.sailing.xrr.resultimport.schema.RegattaResults;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.ReplicationService;
import com.sap.sse.replication.impl.AbstractReplicablesProvider;
import com.sap.sse.replication.impl.ReplicationInstancesManager;
import com.sap.sse.replication.impl.ReplicationServiceImpl;
@@ -60,24 +60,20 @@ public class SailingServiceImplMock extends SailingServiceImpl {
}
@Override
protected ServiceTracker<ReplicationService<RacingEventService>, ReplicationService<RacingEventService>> createAndOpenReplicationServiceTracker(
protected ServiceTracker<ReplicationService, ReplicationService> createAndOpenReplicationServiceTracker(
BundleContext context) {
@SuppressWarnings("unchecked")
ServiceTracker<ReplicationService<RacingEventService>, ReplicationService<RacingEventService>> result = mock(ServiceTracker.class);
ServiceTracker<ReplicationService, ReplicationService> result = mock(ServiceTracker.class);
try {
final ReplicationServiceImpl<RacingEventService, RacingEventServiceOperation<?>> replicationService =
new ReplicationServiceImpl<RacingEventService, RacingEventServiceOperation<?>>("test exchange", "localhost", 0,
new ReplicationInstancesManager<RacingEventService>()) {
@Override
protected ServiceTracker<Replicable<RacingEventService, RacingEventServiceOperation<?>>, Replicable<RacingEventService, RacingEventServiceOperation<?>>> getReplicableTracker() {
@SuppressWarnings("unchecked")
ServiceTracker<Replicable<RacingEventService, RacingEventServiceOperation<?>>, Replicable<RacingEventService, RacingEventServiceOperation<?>>> result =
(ServiceTracker<Replicable<RacingEventService, RacingEventServiceOperation<?>>, Replicable<RacingEventService, RacingEventServiceOperation<?>>>) mock(ServiceTracker.class);
doReturn("Humba Humba").when(result).toString();
when(result.getService()).thenReturn(new RacingEventServiceImpl());
return result;
}
};
final RacingEventService replicable = new RacingEventServiceImpl();
final ReplicationServiceImpl replicationService =
new ReplicationServiceImpl("test exchange", "localhost", 0,
new ReplicationInstancesManager(), new AbstractReplicablesProvider() {
@Override
public Iterable<Replicable<?, ?>> getReplicables() {
return Collections.<Replicable<?, ?>>singleton(replicable);
}
});
when(result.getService()).thenReturn(replicationService);
} catch (IOException e) {
throw new RuntimeException();
@@ -419,7 +419,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
private final ServiceTracker<RacingEventService, RacingEventService> racingEventServiceTracker;
private final ServiceTracker<ReplicationService<RacingEventService>, ReplicationService<RacingEventService>> replicationServiceTracker;
private final ServiceTracker<ReplicationService, ReplicationService> replicationServiceTracker;
private final ServiceTracker<ResultUrlRegistry, ResultUrlRegistry> resultUrlRegistryServiceTracker;
@@ -662,10 +662,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
return new ScoreCorrectionProviderDTO(scoreCorrectionProvider.getName(), hasResultsForBoatClassFromDateByEventName);
}
protected ServiceTracker<ReplicationService<RacingEventService>, ReplicationService<RacingEventService>> createAndOpenReplicationServiceTracker(
protected ServiceTracker<ReplicationService, ReplicationService> createAndOpenReplicationServiceTracker(
BundleContext context) {
ServiceTracker<ReplicationService<RacingEventService>, ReplicationService<RacingEventService>> result =
new ServiceTracker<ReplicationService<RacingEventService>, ReplicationService<RacingEventService>>(
ServiceTracker<ReplicationService, ReplicationService> result =
new ServiceTracker<ReplicationService, ReplicationService>(
context, ReplicationService.class.getName(), null);
result.open();
return result;
@@ -2206,7 +2206,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
return racingEventServiceTracker.getService(); // grab the service
}
private ReplicationService<RacingEventService> getReplicationService() {
private ReplicationService getReplicationService() {
return replicationServiceTracker.getService();
}
@@ -3193,12 +3193,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S
@Override
public ReplicationStateDTO getReplicaInfo() {
ReplicationService<RacingEventService> service = getReplicationService();
ReplicationService service = getReplicationService();
Set<ReplicaDTO> replicaDTOs = new HashSet<ReplicaDTO>();
for (ReplicaDescriptor replicaDescriptor : service.getReplicaInfo()) {
final Map<Class<? extends OperationWithResult<RacingEventService, ?>>, Integer> statistics = service.getStatistics(replicaDescriptor);
final Map<Class<? extends OperationWithResult<?, ?>>, Integer> statistics = service.getStatistics(replicaDescriptor);
Map<String, Integer> replicationCountByOperationClassName = new HashMap<String, Integer>();
for (Entry<Class<? extends OperationWithResult<RacingEventService, ?>>, Integer> e : statistics.entrySet()) {
for (Entry<Class<? extends OperationWithResult<?, ?>>, Integer> e : statistics.entrySet()) {
replicationCountByOperationClassName.put(e.getKey().getName(), e.getValue());
}
replicaDTOs.add(new ReplicaDTO(replicaDescriptor.getIpAddress().getHostName(), replicaDescriptor
@@ -39,6 +39,7 @@ import com.sap.sailing.server.RacingEventServiceOperation;
import com.sap.sailing.server.impl.RacingEventServiceImpl;
import com.sap.sse.common.Util;
import com.sap.sse.mongodb.MongoDBService;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.ReplicationMasterDescriptor;
import com.sap.sse.replication.ReplicationService;
import com.sap.sse.replication.impl.Activator;
@@ -56,7 +57,7 @@ public abstract class AbstractServerReplicationTest {
protected RacingEventServiceImpl master;
protected ReplicationServiceTestImpl replicaReplicator;
private ReplicaDescriptor replicaDescriptor;
private ReplicationServiceImpl<RacingEventService, RacingEventServiceOperation<?>> masterReplicator;
private ReplicationServiceImpl masterReplicator;
private ReplicationMasterDescriptor masterDescriptor;
@Rule public Timeout AbstractTracTracLiveTestTimeout = new Timeout(5 * 60 * 1000); // timeout after 5 minutes
@@ -167,17 +168,17 @@ public abstract class AbstractServerReplicationTest {
replicaReplicator.stopToReplicateFromMaster();
}
static class ReplicationServiceTestImpl extends ReplicationServiceImpl<RacingEventService, RacingEventServiceOperation<?>> {
static class ReplicationServiceTestImpl extends ReplicationServiceImpl {
protected static final int INITIAL_LOAD_PACKAGE_SIZE = 1024*1024;
private final DomainFactory resolveAgainst;
private final RacingEventService master;
private final ReplicaDescriptor replicaDescriptor;
private final ReplicationService<RacingEventService> masterReplicationService;
private final ReplicationService masterReplicationService;
private final ReplicationMasterDescriptor masterDescriptor;
public ReplicationServiceTestImpl(String exchangeName, String exchangeHost, DomainFactory resolveAgainst,
ReplicationInstancesManager<RacingEventService> replicationInstancesManager, ReplicaDescriptor replicaDescriptor,
RacingEventService replica, RacingEventService master, ReplicationService<RacingEventService> masterReplicationService,
ReplicationInstancesManager replicationInstancesManager, ReplicaDescriptor replicaDescriptor,
RacingEventService replica, RacingEventService master, ReplicationService masterReplicationService,
ReplicationMasterDescriptor masterDescriptor)
throws IOException {
super(exchangeName, exchangeHost, replicationInstancesManager, replica);
@@ -248,17 +249,6 @@ public abstract class AbstractServerReplicationTest {
return initialLoadTestServerThread;
}
/**
* Ignore the master descriptor and replicate from the local master passed to the constructor instead.
*/
// @Override
// public void startToReplicateFrom(ReplicationMasterDescriptor master) throws IOException,
// ClassNotFoundException {
// Replicator replicator = startToReplicateFromButDontYetFetchInitialLoad(master, /* startReplicatorSuspended */ true);
// initialLoad();
// replicator.setSuspended(false); // resume after initial load
// }
protected Replicator<RacingEventService, RacingEventServiceOperation<?>> startToReplicateFromButDontYetFetchInitialLoad(ReplicationMasterDescriptor master, boolean startReplicatorSuspended)
throws IOException {
masterReplicationService.registerReplica(replicaDescriptor);
@@ -291,10 +281,11 @@ public abstract class AbstractServerReplicationTest {
}
}
}.start();
ObjectInputStream dis = resolveAgainst.createObjectInputStreamResolvingAgainstThisFactory(pis);
getReplicable().clearReplicaState();
getReplicable().initiallyFillFrom(dis);
dis.close();
for (Replicable<?, ?> replicable : getReplicables()) {
replicable.clearReplicaState();
replicable.initiallyFillFrom(pis);
}
pis.close();
}
}
}
@@ -69,29 +69,37 @@ import com.sap.sse.common.Util.Triple;
import com.sap.sse.common.search.KeywordQuery;
import com.sap.sse.common.search.Result;
import com.sap.sse.common.search.Searchable;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.impl.ReplicableWithObjectInputStream;
/**
* An OSGi service that can be used to track boat races using a TracTrac connector that pushes
* live GPS boat location, waypoint, coarse and mark passing data.<p>
* An OSGi service that can be used to track boat races using a TracTrac connector that pushes live GPS boat location,
* waypoint, coarse and mark passing data.
* <p>
*
* If a race/regatta is already being tracked, another {@link #addTracTracRace(URL, URI, URI, WindStore, long)} or
* {@link #addRegatta(URL, URI, URI, WindStore, long)} call will have no effect, even if a different
* {@link WindStore} is requested.<p>
* {@link #addRegatta(URL, URI, URI, WindStore, long)} call will have no effect, even if a different {@link WindStore}
* is requested.
* <p>
*
* When the tracking of a race/regatta is {@link #stopTracking(Regatta, RaceDefinition) stopped}, the next
* time it's started to be tracked, a new {@link TrackedRace} at least will be constructed. This also
* means that when a {@link TrackedRegatta} exists that still holds other {@link TrackedRace}s, the
* no longer tracked {@link TrackedRace} will be removed from the {@link TrackedRegatta}.
* corresponding information is removed also from the {@link DomainFactory}'s caches to ensure that
* clean, fresh data is received should another tracking request be issued later.
* When the tracking of a race/regatta is {@link #stopTracking(Regatta, RaceDefinition) stopped}, the next time it's
* started to be tracked, a new {@link TrackedRace} at least will be constructed. This also means that when a
* {@link TrackedRegatta} exists that still holds other {@link TrackedRace}s, the no longer tracked {@link TrackedRace}
* will be removed from the {@link TrackedRegatta}. corresponding information is removed also from the
* {@link DomainFactory}'s caches to ensure that clean, fresh data is received should another tracking request be issued
* later.
* <p>
*
* During receiving the initial load for a replication in {@link #initiallyFillFromInternal(java.io.ObjectInputStream)},
* tracked regattas read from the stream are observed (see {@link RaceListener}) by this object for automatic updates to
* the default leaderboard and for automatic linking to leaderboard columns. It is assumed that no explicit replication
* of these operations will happen based on the changes performed on the replication master.
*
* @author Axel Uhl (d043530)
*
*/
public interface RacingEventService extends TrackedRegattaRegistry, RegattaFetcher, RegattaRegistry, RaceFetcher,
LeaderboardRegistry, EventResolver, LeaderboardGroupResolver, TrackerManager, Searchable<LeaderboardSearchResult, KeywordQuery>,
Replicable<RacingEventService, RacingEventServiceOperation<?>> {
ReplicableWithObjectInputStream<RacingEventService, RacingEventServiceOperation<?>> {
@Override
Regatta getRegatta(RegattaName regattaName);
@@ -325,8 +325,7 @@ public class RacingEventServiceImpl implements RacingEventServiceWithTestSupport
/**
* This author should be used for server generated race log events
*/
private final RaceLogEventAuthor raceLogEventAuthorForServer = new RaceLogEventAuthorImpl(
RacingEventService.class.getName(), 0);
private final RaceLogEventAuthor raceLogEventAuthorForServer = new RaceLogEventAuthorImpl(RacingEventService.class.getName(), 0);
/**
* Allow only one master data import at a time to avoid situation where multiple Imports override each other in
@@ -2129,7 +2128,7 @@ public class RacingEventServiceImpl implements RacingEventServiceWithTestSupport
}
@Override
public void serializeForInitialReplication(ObjectOutputStream oos) throws IOException {
public void serializeForInitialReplicationInternal(ObjectOutputStream oos) throws IOException {
StringBuffer logoutput = new StringBuffer();
logger.info("Serializing events...");
@@ -2191,8 +2190,7 @@ public class RacingEventServiceImpl implements RacingEventServiceWithTestSupport
@SuppressWarnings("unchecked") // all the casts of ois.readObject()'s return value to Map<..., ...>
// the type-parameters in the casts of the de-serialized collection objects can't be checked
@Override
public void initiallyFillFrom(ObjectInputStream ois) throws IOException, ClassNotFoundException,
InterruptedException {
public void initiallyFillFromInternal(ObjectInputStream ois) throws IOException, ClassNotFoundException, InterruptedException {
logger.info("Performing initial replication load on " + this);
ClassLoader oldContextClassloader = Thread.currentThread().getContextClassLoader();
try {
@@ -9,7 +9,6 @@ import java.util.UUID;
import com.sap.sailing.domain.base.Competitor;
import com.sap.sailing.domain.base.CompetitorStore;
import com.sap.sailing.domain.base.DomainFactory;
import com.sap.sailing.domain.base.ObjectInputStreamResolvingAgainstDomainFactory;
import com.sap.sailing.domain.base.Regatta;
import com.sap.sailing.domain.base.impl.MasterDataImportInformation;
import com.sap.sailing.domain.base.impl.RegattaImpl;
@@ -20,6 +19,7 @@ import com.sap.sailing.domain.persistence.MongoRaceLogStoreFactory;
import com.sap.sailing.domain.racelog.RaceLogStore;
import com.sap.sailing.server.RacingEventService;
import com.sap.sailing.server.operationaltransformation.ImportMasterDataOperation;
import com.sap.sse.replication.impl.ObjectInputStreamResolvingAgainstCache;
public class MasterDataImporter {
private final DomainFactory baseDomainFactory;
@@ -33,7 +33,7 @@ public class MasterDataImporter {
public void importFromStream(InputStream inputStream, UUID importOperationId, boolean override) throws IOException,
ClassNotFoundException {
ObjectInputStreamResolvingAgainstDomainFactory objectInputStream = racingEventService.getBaseDomainFactory()
ObjectInputStreamResolvingAgainstCache<DomainFactory> objectInputStream = racingEventService.getBaseDomainFactory()
.createObjectInputStreamResolvingAgainstThisFactory(inputStream);
racingEventService
.createOrUpdateDataImportProgressWithReplication(importOperationId, 0.03, "Reading Data", 0.5);
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import org.osgi.framework.BundleContext;
@@ -11,33 +12,46 @@ import org.osgi.framework.BundleContext;
import com.sap.sse.common.WithID;
import com.sap.sse.operationaltransformation.Operation;
import com.sap.sse.operationaltransformation.OperationWithTransformationSupport;
import com.sap.sse.replication.impl.ObjectInputStreamResolvingAgainstCache;
/**
* Represents a replicable part of an application. Such a replicable part is usually holder of the application state and
* central transaction / operation processing unit. Processing {@link Operation operations} generally happens by
* {@link Operation#applyTo(Object) applying} operations to this replicable. As such, this replicable plays the
* role of the <code>toState</code> argument in the {@link Operation#applyTo(Object)} method calls.<p>
* Represents a replicable part of an application. Such a replicable part is usually holder of application state and a
* transaction / operation processing unit. Processing {@link Operation operations} generally happens by
* {@link Operation#applyTo(Object) applying} operations to this replicable. As such, this replicable plays the role of
* the <code>toState</code> argument in the {@link Operation#applyTo(Object)} method calls.
* <p>
*
* In addition to being the operation processor, a replicable must be able to serialize its state for the initial
* load of a replica, and it must be able to initialize its state from this serialized form.<p>
* In addition to being the operation processor, a replicable must be able to serialize its state for the initial load
* of a replica, and it must be able to initialize its state from this serialized form. In doing so it is important to
* not include state that is controlled by another replicable. References to such objects need to be <em>transient</em>
* which means they are not serialized together with the initial load of this replica. If such references need to be
* re-established after de-serialization then they need to represented by some key which can later be looked up in the
* other replicable which can be discovered through the OSGi service registry on the receiving end.
* <p>
*
* For operation and initial load serialization, a replicable must provide a specialized {@link ObjectInputStream} that
* ensures that on the receiving end there are no two Java object copies representing the same domain object where this
* matters. This is particularly important for objects with mutable state that otherwise may run inconsistent when only
* one of the multiple copies gets modified. Typically, the object input stream uses some sort of cache and overrides
* the {@link ObjectInputStream#resolveObject} method, and the types whose objects are de-serialized need to co-operate by
* For operation and initial load serialization, a replicable is provided an {@link InputStream} from which to read the
* operations and the initial load. A typical implementation for reading the stream's contents will use a specialized
* {@link ObjectInputStream} such as a specialization of {@link ObjectInputStreamResolvingAgainstCache} that ensures
* that on the receiving end there are no two Java object copies representing the same domain object where this matters.
* This is particularly important for objects with mutable state that otherwise may run inconsistent when only one of
* the multiple copies gets modified. Typically, the object input stream uses some sort of cache and overrides the
* {@link ObjectInputStream#resolveObject} method, and the types whose objects are de-serialized need to co-operate by
* implementing a marker interface telling the specialized object input stream to use the cache when resolving objects
* of that type.<p>
* of that type.
* <p>
*
* Several {@link Replicable} instances can share the same replication service and the same set of replication channels.
* In order to participate in replication, {@link Replicable} objects need to be registered as OSGi services for the
* {@link Replicable} interface. The replication service will dynamically discover the {@link Replicable} objects when
* initial load is requested and when an {@link Operation} is received. The initial load streams as well as each replication
* operation need to identify the {@link Replicable} they refer to. This identification is added as an OSGi service registry
* parameter that is used during service discovery.
* initial load is requested and when an {@link Operation} is received. The initial load streams as well as each
* replication operation need to identify the {@link Replicable} they refer to. This identification is added as an OSGi
* service registry parameter that is used during service discovery.
*
* @param <S> the type of state to which the operations are applied; usually this will be set to the implementing subclass
* @param <O> type of operation that the replicable accepts
* @param <S>
* the type of state to which the operations are applied; usually this will be set to the implementing
* subclass
* @param <O>
* type of operation that the replicable accepts
*
* @author Axel Uhl (D043530)
*
@@ -51,12 +65,6 @@ public interface Replicable<S, O extends OperationWithResult<S, ?>> extends With
*/
final String OSGi_Service_Registry_ID_Property_Name = "ID";
/**
* Produces an object input stream that can choose to resolve objects against a cache so that duplicate instances
* are avoided.
*/
ObjectInputStream createObjectInputStreamResolvingAgainstCache(InputStream is) throws IOException;
/**
* Executes an operation whose effects need to be replicated to any replica of this service known and
* {@link OperationExecutionListener#executed(OperationWithTransformationSupport) notifies} all registered
@@ -64,6 +72,11 @@ public interface Replicable<S, O extends OperationWithResult<S, ?>> extends With
*/
<T> T apply(OperationWithResult<S, T> operation);
/**
* An operation execution listener must be able to process notifications of operations being executed that have
* type <code>S</code> or any more specific type. The listener can achieve this also by accepting any type more general than
* <code>S</code>.
*/
void addOperationExecutionListener(OperationExecutionListener<S> listener);
void removeOperationExecutionListener(OperationExecutionListener<S> listener);
@@ -80,27 +93,32 @@ public interface Replicable<S, O extends OperationWithResult<S, ?>> extends With
void clearReplicaState() throws MalformedURLException, IOException, InterruptedException;
/**
* Dual, reading operation for {@link #serializeForInitialReplication(ObjectOutputStream)}. In other words, when
* this operation returns, this service instance is in a state "equivalent" to that of the service instance that
* produced the stream contents in its {@link #serializeForInitialReplication(ObjectOutputStream)}. "Equivalent"
* here means that a replica will have equal sets of regattas, tracked regattas, leaderboards and leaderboard groups but
* will not have any active trackers for wind or positions because it relies on these elements to be sent through
* the replication channel.
* Dual, reading operation for {@link #serializeForInitialReplication(OutputStream)}. In other words, when this
* operation returns, this service instance is in a state "equivalent" to that of the service instance that produced
* the stream contents in its {@link #serializeForInitialReplication(OutputStream)}. "Equivalent" here means that a
* replica will have equal sets of objects serialized in the initial load but will usually not have any transient
* processes replicated that are responsible for building and maintaining the state on the master side because it
* relies on these elements to be sent through the replication channel.
* <p>
*
* Tracked regattas read from the stream are observed (see {@link RaceListener}) by this object for automatic updates
* to the default leaderboard and for automatic linking to leaderboard columns. It is assumed that no explicit
* replication of these operations will happen based on the changes performed on the replication master.<p>
*
* <b>Caution:</b> All relevant contents of this service instance needs to be cleared before by a call to
* {@link #clearReplicaState()}. It will be replaced by the stream contents.
*/
void initiallyFillFrom(ObjectInputStream ois) throws IOException, ClassNotFoundException, InterruptedException;
void initiallyFillFrom(InputStream is) throws IOException, ClassNotFoundException, InterruptedException;
/**
* Produces a one-shot serializable copy of those elements required for replication into <code>oos</code> so that
* afterwards the {@link RacingEventServiceOperation}s can be {@link #apply(RacingEventServiceOperation) applied} to
* maintain consistency with the master copy of the service. The dual operation is {@link #initiallyFillFrom}.
* Produces a one-shot serializable copy of those elements required for replication into <code>os</code> by wrapping
* that stream by an {@link ObjectOutputStream} so that afterwards the {@link OperationWithResult}s can be
* {@link #apply(OperationWithResult) applied} to maintain consistency with the master copy of the service. The dual
* operation is {@link #initiallyFillFrom}.
*/
void serializeForInitialReplication(ObjectOutputStream oos) throws IOException;
void serializeForInitialReplication(OutputStream os) throws IOException;
/**
* From an input stream, reads an operation that can be {@link #apply(OperationWithResult) applied} to this object.
* Separating reading and applying gives clients an opportunity to queue operations, e.g., in order to wait until
* receiving and {@link #initiallyFillFrom(InputStream) filling} the initial load has completed.
* @throws ClassNotFoundException
*/
O readOperation(InputStream inputStream) throws IOException, ClassNotFoundException;
}
@@ -0,0 +1,33 @@
package com.sap.sse.replication;
public interface ReplicablesProvider {
/**
* Listeners of this type can be registered and de-registered using
* {@link ReplicablesProvider#addReplicableLifeCycleListener(ReplicableLifeCycleListener)} and
* {@link ReplicablesProvider#removeReplicableLifeCycleListener(ReplicableLifeCycleListener)}. They will be notified of
* {@link Replicable}s being added to or removed from this server instance. A replication service may use this to
* start listening to operations executed by a new {@link Replicable} or stop observing one that is being removed.
*
* @author Axel Uhl (D043530)
*
*/
public static interface ReplicableLifeCycleListener {
void replicableAdded(Replicable<?, ?> replicable);
void replicableRemoved(Replicable<?, ?> replicable);
}
Iterable<Replicable<?, ?>> getReplicables();
/**
* Tries to obtain a {@link Replicable> by its {@link Replicable#getId() ID}, converted to a string using {@link Object#toString()}
* on the ID.
*
* @return <code>null</code> if no such replicable can be found; the replicable otherwise
*/
Replicable<?, ?> getReplicable(String replicableIdAsString);
void addReplicableLifeCycleListener(ReplicableLifeCycleListener listener);
void removeReplicableLifeCycleListener(ReplicableLifeCycleListener listener);
}
@@ -23,10 +23,13 @@ public interface ReplicationMasterDescriptor {
URL getReplicationDeRegistrationRequestURL(UUID uuid) throws MalformedURLException;
/**
* The content produced by the URL returned is the name of the queue from which the client can read the
* initial load using a {@link RabbitInputStreamProvider}.
* The content produced by the URL returned is the name of the queue from which the client can read the initial load
* using a {@link RabbitInputStreamProvider}.
*
* @param replicables
* the replicables for which to request the initial load
*/
URL getInitialLoadURL() throws MalformedURLException;
URL getInitialLoadURL(Iterable<Replicable<?, ?>> replicables) throws MalformedURLException;
int getMessagingPort();
@@ -16,7 +16,7 @@ import com.sap.sse.replication.impl.ReplicationServlet;
*
* @param <S> the type representing the master's state
*/
public interface ReplicationService<S> {
public interface ReplicationService {
static String SAILING_SERVER_REPLICATION_TOPIC = "SailingServerReplicationTopic";
/**
@@ -57,7 +57,7 @@ public interface ReplicationService<S> {
* For a replica replicating off this master, provides statistics in the form of number of operations sent to that
* replica by type, where the operation type is the key, represented as the operation's class name
*/
Map<Class<? extends OperationWithResult<S, ?>>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor);
Map<Class<? extends OperationWithResult<?, ?>>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor);
double getAverageNumberOfOperationsPerMessage(ReplicaDescriptor replicaDescriptor);
@@ -0,0 +1,39 @@
package com.sap.sse.replication.impl;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.stream.StreamSupport;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.ReplicablesProvider;
public abstract class AbstractReplicablesProvider implements ReplicablesProvider {
private final Set<ReplicableLifeCycleListener> listeners;
public AbstractReplicablesProvider() {
this.listeners = new HashSet<>();
}
@Override
public void addReplicableLifeCycleListener(ReplicableLifeCycleListener listener) {
listeners.add(listener);
}
@Override
public void removeReplicableLifeCycleListener(ReplicableLifeCycleListener listener) {
listeners.remove(listener);
}
/**
* This default implementation loops over the result of {@link #getReplicables()} and returns the first object whose
* {@link Replicable#getId() ID} equals the value of the parameter <code>replicableIdAsString</code>.
*
* @throws NoSuchElementException if no replicable by that name is found
*/
@Override
public Replicable<?, ?> getReplicable(String replicableIdAsString) {
return StreamSupport.stream(getReplicables().spliterator(), /* parallel */false)
.filter(r -> r.getId().toString().equals(replicableIdAsString)).findAny().orElse(null);
}
}
@@ -74,7 +74,7 @@ public class Activator implements BundleActivator {
public static final String PROPERTY_NAME_REPLICATE_MASTER_QUEUE_PORT = "replicate.master.queue.port";
public static final String PROPERTY_NAME_REPLICATE_MASTER_EXCHANGE_NAME = "replicate.master.exchange.name";
private ReplicationInstancesManager<?> replicationInstancesManager;
private ReplicationInstancesManager replicationInstancesManager;
private static BundleContext defaultContext;
@@ -110,15 +110,15 @@ public class Activator implements BundleActivator {
} catch (NumberFormatException nfe) {
logger.severe("Couldn't parse the replication port specification \""+exchangePortAsString+"\". Using default.");
}
replicationInstancesManager = new ReplicationInstancesManager<>();
ReplicationService<?> serverReplicationMasterService = new ReplicationServiceImpl<>(
exchangeName, exchangeHost, exchangePort, replicationInstancesManager);
replicationInstancesManager = new ReplicationInstancesManager();
ReplicationService serverReplicationMasterService = new ReplicationServiceImpl(
exchangeName, exchangeHost, exchangePort, replicationInstancesManager, new OSGiReplicableTracker(bundleContext));
bundleContext.registerService(ReplicationService.class, serverReplicationMasterService, null);
logger.info("Registered replication service "+serverReplicationMasterService+" using exchange name "+exchangeName+" on host "+exchangeHost);
checkIfAutomaticReplicationShouldStart(serverReplicationMasterService, exchangeName);
}
private void checkIfAutomaticReplicationShouldStart(ReplicationService<?> serverReplicationMasterService, String masterExchangeName) {
private void checkIfAutomaticReplicationShouldStart(ReplicationService serverReplicationMasterService, String masterExchangeName) {
String replicateOnStart = System.getProperty(PROPERTY_NAME_REPLICATE_ON_START);
if (Boolean.valueOf(replicateOnStart)) {
logger.info("Configuration requested automatic replication. Starting it up...");
@@ -1,8 +0,0 @@
package com.sap.sse.replication.impl;
import com.sap.sse.replication.OperationWithResult;
import com.sap.sse.replication.Replicable;
public interface HasReplicable<S, O extends OperationWithResult<S, ?>> {
Replicable<S, O> getReplicable();
}
@@ -0,0 +1,63 @@
package com.sap.sse.replication.impl;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
import org.osgi.util.tracker.ServiceTracker;
import org.osgi.util.tracker.ServiceTrackerCustomizer;
import com.sap.sse.replication.Replicable;
/**
* Uses an OSGi {@link ServiceTracker} to track and follow the set of {@link Replicable} objects registered with
* the OSGi service registry under that interface and uses the information and the events from the tracker to
* prepare the responses to the {@link #getReplicable(String)} and {@link #getReplicables()} method calls.
*
* @author Axel Uhl (D043530)
*
*/
public class OSGiReplicableTracker extends AbstractReplicablesProvider {
private final ServiceTracker<Replicable<?, ?>, Replicable<?, ?>> serviceTracker;
private final Map<String, ServiceReference<Replicable<?, ?>>> serviceReferenceByIdAsString;
private final Map<ServiceReference<Replicable<?, ?>>, String> idAsStringByServiceReference;
private final BundleContext bundleContext;
public OSGiReplicableTracker(final BundleContext bundleContext) {
this.bundleContext = bundleContext;
this.serviceReferenceByIdAsString = new HashMap<>();
this.idAsStringByServiceReference = new HashMap<>();
this.serviceTracker = new ServiceTracker<Replicable<?, ?>, Replicable<?, ?>>(
bundleContext, Replicable.class.getName(), new ServiceTrackerCustomizer<Replicable<?, ?>, Replicable<?, ?>>() {
@Override
public Replicable<?, ?> addingService(ServiceReference<Replicable<?, ?>> reference) {
final Replicable<?, ?> result = bundleContext.getService(reference);
serviceReferenceByIdAsString.put(result.getId().toString(), reference);
idAsStringByServiceReference.put(reference, result.getId().toString());
return result;
}
@Override
public void removedService(ServiceReference<Replicable<?, ?>> reference,
Replicable<?, ?> service) {
serviceReferenceByIdAsString.remove(idAsStringByServiceReference.get(reference));
idAsStringByServiceReference.remove(reference);
}
@Override public void modifiedService(ServiceReference<Replicable<?, ?>> reference, Replicable<?, ?> service) {}
});
serviceTracker.open();
}
@Override
public Iterable<Replicable<?, ?>> getReplicables() {
return Arrays.asList(serviceTracker.getServices(new Replicable<?, ?>[0]));
}
@Override
public Replicable<?, ?> getReplicable(String replicableIdAsString) {
return bundleContext.getService(serviceReferenceByIdAsString.get(replicableIdAsString));
}
}
@@ -9,15 +9,22 @@ import com.sap.sse.common.IsManagedByCache;
import com.sap.sse.replication.Replicable;
/**
* During de-serialization, resolves objects managed by a {@link DomainFactory} using the domain factory instance passed
* to the constructor. Additionally, class loading is managed using the current thread's context class loader which is
* required in and works well for OSGi environments.
* During de-serialization, resolves objects managed by a cache of type <C> using a cache of that type passed to the
* constructor. Objects are identified as being managed by such a cache by means of the marker interface
* {@link IsManagedByCache} that their classes then have to implement, telling how resolving against the cache
* has to work for instances of that type.
* <p>
*
* Create an instance of this class using
* {@link DomainFactory#createObjectInputStreamResolvingAgainstThisFactory(InputStream)}.
* Additionally, class loading is managed using the current thread's context class loader which is required in and works
* well for OSGi environments.
* <p>
*
* @param <C> the cache type used for resolving objects
* Create an instance of this class by first creating a subclass in the bundle that has all the dependencies required to
* resolve all the classes whose instances are to be read from the stream and then instantiating this subclass. Otherwise,
* the stream won't be able to find the classes. To make this need clear, this class is declared <code>abstract</code>.
*
* @param <C>
* the cache type used for resolving objects
*
* @author Axel Uhl (D043530)
*
@@ -0,0 +1,59 @@
package com.sap.sse.replication.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import com.sap.sse.replication.OperationWithResult;
import com.sap.sse.replication.Replicable;
public interface ReplicableWithObjectInputStream<S, O extends OperationWithResult<S, ?>> extends Replicable<S, O> {
/**
* Produces an object input stream that can choose to resolve objects against a cache so that duplicate instances
* are avoided.
*/
ObjectInputStream createObjectInputStreamResolvingAgainstCache(InputStream is) throws IOException;
/**
* Implementation of {@link #initiallyFillFrom(InputStream)} which receives an {@link ObjectInputStream} instead of
* an {@link InputStream}. The {@link ObjectInputStream} is expected to have been produced by
* {@link #createObjectInputStreamResolvingAgainstCache(InputStream)}.
*/
void initiallyFillFromInternal(ObjectInputStream is) throws IOException, ClassNotFoundException, InterruptedException;
/**
* Implementation of {@link #serializeForInitialReplication(OutputStream)}, using an {@link ObjectOutputStream}.
*/
void serializeForInitialReplicationInternal(ObjectOutputStream objectOutputStream) throws IOException;
/**
* Implementation of {@link #readOperation(InputStream)}, using the {@link ObjectInputStream} created by
* {@link #createObjectInputStreamResolvingAgainstCache(InputStream)}.
*/
@SuppressWarnings("unchecked")
default O readOperationInternal(ObjectInputStream ois) throws ClassNotFoundException, IOException {
return (O) ois.readObject();
}
@Override
default void initiallyFillFrom(InputStream is) throws IOException, ClassNotFoundException, InterruptedException {
initiallyFillFromInternal(createObjectInputStreamResolvingAgainstCache(is));
}
/**
* Wraps <code>os</code> by an {@link ObjectOutputStream} and invokes
* {@link #serializeForInitialReplicationInternal(ObjectOutputStream)}.
*/
@Override
default void serializeForInitialReplication(OutputStream os) throws IOException {
serializeForInitialReplicationInternal(new ObjectOutputStream(os));
}
@Override
default O readOperation(InputStream inputStream) throws IOException, ClassNotFoundException {
return readOperationInternal(createObjectInputStreamResolvingAgainstCache(inputStream));
}
}
@@ -18,7 +18,7 @@ import com.sap.sse.replication.ReplicationMasterDescriptor;
*
* @param <S> the state to which replication operations are applied
*/
public class ReplicationInstancesManager<S> {
public class ReplicationInstancesManager {
/**
* The set of descriptors of all registered slaves. All broadcast operations will send the messages to all
@@ -26,7 +26,7 @@ public class ReplicationInstancesManager<S> {
*/
private Set<ReplicaDescriptor> replicaDescriptors;
private Map<ReplicaDescriptor, Map<Class<? extends OperationWithResult<S, ?>>, Integer>> replicationCounts;
private Map<ReplicaDescriptor, Map<Class<? extends OperationWithResult<?, ?>>, Integer>> replicationCounts;
/**
* Used to calculate the average number of operations per message sent/received for each replica.
@@ -54,7 +54,7 @@ public class ReplicationInstancesManager<S> {
public ReplicationInstancesManager() {
replicaDescriptors = new HashSet<ReplicaDescriptor>();
replicationCounts = new HashMap<ReplicaDescriptor, Map<Class<? extends OperationWithResult<S, ?>>,Integer>>();
replicationCounts = new HashMap<ReplicaDescriptor, Map<Class<? extends OperationWithResult<?, ?>>,Integer>>();
totalMessageCount = new HashMap<>();
totalNumberOfOperations = new HashMap<>();
totalQueueMessagesRawSizeInBytes = new HashMap<ReplicaDescriptor, Long>();
@@ -80,7 +80,7 @@ public class ReplicationInstancesManager<S> {
public void registerReplica(ReplicaDescriptor replica) {
replicaDescriptors.add(replica);
replicationCounts.put(replica, new HashMap<Class<? extends OperationWithResult<S, ?>>, Integer>());
replicationCounts.put(replica, new HashMap<Class<? extends OperationWithResult<?, ?>>, Integer>());
}
public void unregisterReplica(ReplicaDescriptor replica) {
@@ -96,15 +96,15 @@ public class ReplicationInstancesManager<S> {
*/
public void log(List<Class<?>> classes, long sizeOfQueueMessageInBytes) {
for (ReplicaDescriptor replica : getReplicaDescriptors()) {
Map<Class<? extends OperationWithResult<S, ?>>, Integer> counts = replicationCounts.get(replica);
Map<Class<? extends OperationWithResult<?, ?>>, Integer> counts = replicationCounts.get(replica);
if (counts == null) {
counts = new HashMap<Class<? extends OperationWithResult<S, ?>>, Integer>();
counts = new HashMap<Class<? extends OperationWithResult<?, ?>>, Integer>();
replicationCounts.put(replica, counts);
}
for (Class<?> replicatedOperationClass : classes) {
// safe because replicatedOperation is declared of type RacingEventserviceOperation<T>
@SuppressWarnings("unchecked")
Class<? extends OperationWithResult<S, ?>> operationClass = (Class<? extends OperationWithResult<S, ?>>) replicatedOperationClass;
Class<? extends OperationWithResult<?, ?>> operationClass = (Class<? extends OperationWithResult<?, ?>>) replicatedOperationClass;
Integer count = counts.get(operationClass);
if (count == null) {
count = 0;
@@ -130,7 +130,7 @@ public class ReplicationInstancesManager<S> {
}
}
public Map<Class<? extends OperationWithResult<S, ?>>, Integer> getStatistics(ReplicaDescriptor replica) {
public Map<Class<? extends OperationWithResult<?, ?>>, Integer> getStatistics(ReplicaDescriptor replica) {
return replicationCounts.get(replica);
}
@@ -13,6 +13,7 @@ import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import com.sap.sse.BuildVersion;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.ReplicationMasterDescriptor;
public class ReplicationMasterDescriptorImpl implements ReplicationMasterDescriptor {
@@ -72,7 +73,7 @@ public class ReplicationMasterDescriptorImpl implements ReplicationMasterDescrip
}
@Override
public URL getInitialLoadURL() throws MalformedURLException {
public URL getInitialLoadURL(Iterable<Replicable<?, ?>> replicables) throws MalformedURLException {
return new URL("http", getHostname(), servletPort, REPLICATION_SERVLET + "?" + ReplicationServlet.ACTION + "="
+ ReplicationServlet.Action.INITIAL_LOAD.name());
}
@@ -0,0 +1,47 @@
package com.sap.sse.replication.impl;
import com.sap.sse.replication.OperationExecutionListener;
import com.sap.sse.replication.OperationWithResult;
import com.sap.sse.replication.Replicable;
/**
* Listens for operation execution notifications from a single {@link Replicable} object. It asks the
* {@link ReplicationServiceImpl} it was passed at construction to
* {@link ReplicationServiceImpl#broadcastOperation(OperationWithResult, Replicable<S, O>) broadcast} the operation to any interested
* replica.
*
* @author Axel Uhl (D043530)
*
* @param <S>
*/
public class ReplicationServiceExecutionListener<S> implements OperationExecutionListener<S> {
private final ReplicationServiceImpl replicationService;
private final Replicable<S, ?> replicable;
/**
* {@link Replicable#addOperationExecutionListener(OperationExecutionListener) Adds} this new listener to the <code>replicable</code>
*/
protected ReplicationServiceExecutionListener(ReplicationServiceImpl replicationService, Replicable<S, ?> replicable) {
super();
this.replicationService = replicationService;
this.replicable = replicable;
this.replicable.addOperationExecutionListener(this);
}
/**
* {@link #broadcastOperation(OperationWithResult, Replicable<S, O>) Broadcasts} the <code>operation</code> to all registered
* replicas by publishing it to the fan-out exchange.
*/
@Override
public <T> void executed(OperationWithResult<S, T> operation) {
try {
replicationService.broadcastOperation(operation, replicable);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public void unsubscribe() {
this.replicable.removeOperationExecutionListener(this);
}
}
@@ -2,18 +2,20 @@ package com.sap.sse.replication.impl;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.ConnectException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;
@@ -30,37 +32,48 @@ import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.QueueingConsumer;
import com.sap.sse.BuildVersion;
import com.sap.sse.common.Util;
import com.sap.sse.replication.OperationExecutionListener;
import com.sap.sse.replication.OperationWithResult;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.ReplicablesProvider;
import com.sap.sse.replication.ReplicablesProvider.ReplicableLifeCycleListener;
import com.sap.sse.replication.ReplicationMasterDescriptor;
import com.sap.sse.replication.ReplicationService;
/**
* Can observe a {@link Replicable} for the operations it performs that require replication. Only observes as long as
* there are replicas registered. If the last replica is de-registered, the service stops observing the
* {@link Replicable}. Operations received that require replication are sent to the {@link Exchange} to which replica
* queues can bind, using a {@link Replicator}. The exchange name and connectivity information for the message queuing
* system is provided to this service during construction.
* Manages a set of observers of {@link Replicable}, receiving notifications for the operations they perform that
* require replication. This service provides the connectivity and central management including central keeping of
* replication statistics for this server instance. Triggering the broadcast of an operation notified by a
* {@link Replicable} is in the responsibility of each individual observer.
* <p>
*
* This service object {@link Replicable#addOperationExecutionListener(OperationExecutionListener) registers} as
* listener at the {@link Replicable} so it {@link #executed(OperationWithResult) receives} notifications about
* operations executed by the {@link Replicable} that require replication if and only if there is at least one
* replica registered.
* The observers are registered only when there are replicas registered. If the last replica is de-registered, the
* service stops observing the {@link Replicable}. Operations received that require replication are sent to the
* {@link Exchange} to which replica queues can bind, using a {@link Replicator}. By prefixing each message with the
* {@link Object#toString()} representation of the {@link Replicable}'s {@link Replicable#getId() ID} the receiver can
* determine to which {@link Replicable} to forward the operation. As such, this service multiplexes the replication
* channels for potentially many {@link Replicable}s living in this server instance.
* <p>
*
* The exchange name and connectivity information for the message queuing system are provided to this service during
* construction.
* <p>
*
* This service object {@link Replicable#addOperationExecutionListener(OperationExecutionListener) registers} individual
* listeners at the {@link Replicable}s so it {@link #executed(OperationWithResult) receives} notifications about
* operations executed by the {@link Replicable} that require replication if and only if there is at least one replica
* registered.
*
* @author Frank Mittag, Axel Uhl (d043530)
*
*/
public class ReplicationServiceImpl<S, O extends OperationWithResult<S, ?>> implements ReplicationService<S>,
OperationExecutionListener<S>, HasReplicable<S, O> {
public class ReplicationServiceImpl implements ReplicationService {
private static final Logger logger = Logger.getLogger(ReplicationServiceImpl.class.getName());
private final ReplicationInstancesManager<S> replicationInstancesManager;
private final ReplicationInstancesManager replicationInstancesManager;
private final ServiceTracker<Replicable<S, O>, Replicable<S, O>> replicableTracker;
private final Replicable<S, O> localService;
private final ReplicablesProvider replicablesProvider;
/**
* <code>null</code>, if this instance is not currently replicating from some master; the master's descriptor otherwise
@@ -102,7 +115,9 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
* For this instance running as a replica, the replicator receives messages from the master's queue and applies them
* to the local replica.
*/
private Replicator<S, O> replicator;
private Replicator replicator;
private final Set<ReplicationServiceExecutionListener<?>> executionListeners;
private Thread replicatorThread;
@@ -120,6 +135,17 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
* are replaced or cleared when the timer scoops up the currently buffered operations to send them out.
*/
private ByteArrayOutputStream outboundBuffer;
/**
* The {@link #outboundBuffer} contains a message with serialized operations originating from a single
* {@link Replicable} whose {@link Replicable#getId() ID} is written as its string value to the beginning
* of the stream using {@link DataOutputStream#writeUTF(String)}. When an operation of a {@link Replicable}
* with a different ID is to be {@link #broadcastOperation(OperationWithResult, Replicable) broadcast}, the
* existing {@link #outboundBuffer} needs to be transmitted and a new one is started for the {@link Replicable}
* now wanting to replicate an operation. Access is synchronized, as for {@link #outboundBuffer} using the
* {@link #outboundBufferMonitor}.
*/
private String outboundBufferReplicableIdAsString;
/**
* An object output stream that writes to {@link #outboundBuffer}. Operations are serialized into this stream until the timer
@@ -173,51 +199,52 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
*/
private long messageCount;
/**
* @param exchangeName name of the fan-out exchange to which replication operations will be sent
* @param exchangeHost name of the host under which the RabbitMQ server can be reached
* @param exchangePort port of the RabbitMQ server, or 0 for default port
*/
public ReplicationServiceImpl(String exchangeName, String exchangeHost, int exchangePort, final ReplicationInstancesManager<S> replicationInstancesManager) throws IOException {
this(exchangeName, exchangeHost, exchangePort, replicationInstancesManager, /* localService */ null, /* create ReplicableServiceTracker */ true);
private class LifeCycleListener implements ReplicableLifeCycleListener {
@Override
public void replicableAdded(Replicable<?, ?> replicable) {
addNewOperationExecutionListener(replicable);
}
@Override
public void replicableRemoved(Replicable<?, ?> replicable) {
// TODO
}
}
private ReplicationServiceImpl(String exchangeName, String exchangeHost,
int exchangePort, final ReplicationInstancesManager<S> replicationInstancesManager,
Replicable<S, O> localService, boolean createReplicableServiceTracker) throws IOException {
/**
* @param exchangeName
* name of the fan-out exchange to which replication operations will be sent
* @param exchangeHost
* name of the host under which the RabbitMQ server can be reached
* @param exchangePort
* port of the RabbitMQ server, or 0 for default port
* @param replicablesProvider
* lets this service request the currently known {@link Replicable} objects to be observed for
* replication; it also offers this service the possibility to register for life cycle events of those
* {@link Replicable} objects so that this service can stop observing them for operations to be
* replicated when they have been removed, or start observing new {@link Recpliable} objects that were
* added.
*/
public ReplicationServiceImpl(String exchangeName, String exchangeHost,
int exchangePort, final ReplicationInstancesManager replicationInstancesManager,
ReplicablesProvider replicablesProvider) throws IOException {
timer = new Timer("ReplicationServiceImpl timer for delayed task sending");
executionListeners = new HashSet<>();
this.replicationInstancesManager = replicationInstancesManager;
replicaUUIDs = new HashMap<ReplicationMasterDescriptor, String>();
if (createReplicableServiceTracker) {
replicableTracker = getReplicableTracker();
replicableTracker.open();
} else {
replicableTracker = null;
}
this.localService = localService;
this.replicablesProvider = replicablesProvider;
this.exchangeName = exchangeName;
this.exchangeHost = exchangeHost;
this.exchangePort = exchangePort;
replicablesProvider.addReplicableLifeCycleListener(new LifeCycleListener());
replicator = null;
serverUUID = UUID.randomUUID();
logger.info("Setting " + serverUUID.toString() + " as unique replication identifier.");
}
/**
* Like {@link #ReplicationServiceImpl(String, ReplicationInstancesManager)}, only that instead of using an OSGi
* service tracker to discover the {@link Replicable}, the service to replicate is "injected" here.
*
* @param exchangeName
* the name of the exchange to which replicas can bind
*/
public ReplicationServiceImpl(String exchangeName, String exchangeHost,
final ReplicationInstancesManager<S> replicationInstancesManager, Replicable<S, O> localService) throws IOException {
this(exchangeName, exchangeHost, 0, replicationInstancesManager, localService, /* create ReplicableServiceTracker */ false);
}
protected ServiceTracker<Replicable<S, O>, Replicable<S, O>> getReplicableTracker() {
return new ServiceTracker<Replicable<S, O>, Replicable<S, O>>(
protected ServiceTracker<Replicable<?, ?>, Replicable<?, ?>> getReplicableTracker() {
return new ServiceTracker<Replicable<?, ?>, Replicable<?, ?>>(
Activator.getDefaultContext(), Replicable.class.getName(), null);
}
@@ -247,15 +274,8 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
return result;
}
@Override
public Replicable<S, O> getReplicable() {
Replicable<S, O> result;
if (localService != null) {
result = localService;
} else {
result = replicableTracker.getService();
}
return result;
private Iterable<Replicable<?, ?>> getReplicables() {
return replicablesProvider.getReplicables();
}
@Override
@@ -273,7 +293,14 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
}
private void addAsListenerToReplicables() {
getReplicable().addOperationExecutionListener(this);
for (Replicable<?, ?> replicable : getReplicables()) {
addNewOperationExecutionListener(replicable);
}
}
private <S> void addNewOperationExecutionListener(Replicable<S, ?> replicable) {
final ReplicationServiceExecutionListener<S> listener = new ReplicationServiceExecutionListener<S>(this, replicable);
executionListeners.add(listener);
}
@Override
@@ -292,29 +319,41 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
}
private void removeAsListenerFromReplicables() {
getReplicable().removeOperationExecutionListener(this);
for (ReplicationServiceExecutionListener<?> listener : executionListeners) {
listener.unsubscribe();
}
}
/**
* Schedules a single operation for broadcast. The operation is added to {@link #outboundBuffer}, and if not already scheduled,
* a {@link #timer} is created and scheduled to send in {@link #TRANSMISSION_DELAY_MILLIS} milliseconds.
* Schedules a single operation for broadcast. The operation is added to {@link #outboundBuffer}, and if not already
* scheduled, a {@link #timer} is created and scheduled to send in {@link #TRANSMISSION_DELAY_MILLIS} milliseconds.
*
* @param replicable
* the replicable by which the operation was executed that now will be broadcast to all replicas; the {@link Replicable#getId() ID}
* of this replica in its string form will be used to prefix the operation
*/
private void broadcastOperation(OperationWithResult<S, ?> operation) throws IOException {
<S, O extends OperationWithResult<S, ?>> void broadcastOperation(OperationWithResult<?, ?> operation, Replicable<S, O> replicable) throws IOException {
// need to write the operations one by one, making sure the ObjectOutputStream always writes
// identical objects again if required because they may have changed state in between
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(operation);
oos.close();
final byte[] bytes = bos.toByteArray();
final byte[] serializedOperation = bos.toByteArray();
synchronized (outboundBufferMonitor) {
final String replicaIdAsString = replicable.getId().toString();
if (outboundBuffer != null && !Util.equalsWithNull(outboundBufferReplicableIdAsString, replicaIdAsString)) {
flushBufferToRabbitMQ(); // operation from a replicable different from that for which operations are buffered so far --> flush
} // still holding the monitor, so no other broadcast request from a different replicable can step in between
if (outboundBuffer == null) {
outboundBuffer = new ByteArrayOutputStream();
outboundBufferReplicableIdAsString = replicaIdAsString;
GZIPOutputStream zipper = new GZIPOutputStream(outboundBuffer);
new DataOutputStream(zipper).writeUTF(replicaIdAsString);
outboundObjectBuffer = new ObjectOutputStream(zipper);
outboundBufferClasses = new ArrayList<>();
}
outboundObjectBuffer.writeObject(bytes);
outboundObjectBuffer.writeObject(serializedOperation);
outboundBufferClasses.add(operation.getClass());
if (outboundBuffer.size() > TRIGGER_MESSAGE_SIZE_IN_BYTES) {
logger.info("Triggering replication because buffer holds " + outboundBuffer.size()
@@ -367,6 +406,7 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
classesOfOperationsToSend = outboundBufferClasses;
doSend = true;
outboundBuffer = null;
outboundBufferReplicableIdAsString = null;
outboundObjectBuffer = null;
outboundBufferClasses = null;
} else {
@@ -407,6 +447,11 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
return replicatingFromMaster;
}
/**
* The peer for this method is
* {@link ReplicationServlet#doGet(javax.servlet.http.HttpServletRequest, javax.servlet.http.HttpServletResponse)}
* which implements the initial load sending process.
*/
@Override
public void startToReplicateFrom(ReplicationMasterDescriptor master) throws IOException, ClassNotFoundException, InterruptedException {
logger.info("Starting to replicate from "+master);
@@ -430,30 +475,35 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
throw ex;
}
logger.info("Connection to exchange successful.");
URL initialLoadURL = master.getInitialLoadURL();
final Iterable<Replicable<?, ?>> replicables = replicablesProvider.getReplicables();
URL initialLoadURL = master.getInitialLoadURL(replicables);
logger.info("Initial load URL is "+initialLoadURL);
replicator = new Replicator<S, O>(master, this, /* startSuspended */ true, consumer);
// start receiving messages already now, but start in suspended mode
replicatorThread = new Thread(replicator, "Replicator receiving from "+master.getMessagingHostname()+"/"+master.getExchangeName());
final Replicable<?, ?> replicable = getReplicable();
// clear Replicable state here, before starting to receive and de-serialized operations which builds up new state, e.g., in competitor store
replicable.clearReplicaState(); // see also bug 2437
replicator = new Replicator(master, replicablesProvider, /* startSuspended */true, consumer);
// clear Replicable state here, before starting to receive and de-serialized operations which builds up
// new state, e.g., in competitor store
for (Replicable<?, ?> r : getReplicables()) {
r.clearReplicaState();
}
replicatorThread = new Thread(replicator, "Replicator receiving from " + master.getMessagingHostname() + "/"
+ master.getExchangeName());
replicatorThread.start();
logger.info("Started replicator thread");
InputStream is = initialLoadURL.openStream();
final String queueName = new BufferedReader(new InputStreamReader(is)).readLine();
RabbitInputStreamProvider rabbitInputStreamProvider = new RabbitInputStreamProvider(master.createChannel(),
queueName);
try {
RabbitInputStreamProvider rabbitInputStreamProvider = new RabbitInputStreamProvider(master.createChannel(), queueName);
ObjectInputStream ois = replicable.createObjectInputStreamResolvingAgainstCache(
new GZIPInputStream(rabbitInputStreamProvider.getInputStream()));
logger.info("Starting to receive initial load");
replicable.initiallyFillFrom(ois);
logger.info("Done receiving initial load");
replicator.setSuspended(false); // apply queued operations
for (Replicable<?, ?> replicable : getReplicables()) {
logger.info("Starting to receive initial load");
replicable.initiallyFillFrom(new GZIPInputStream(rabbitInputStreamProvider.getInputStream()));
logger.info("Done receiving initial load");
replicator.setSuspended(false); // apply queued operations
}
} finally {
// delete initial load queue
DeleteOk deleteOk = consumer.getChannel().queueDelete(queueName);
logger.info("Deleted queue "+queueName+" used for initial load: "+deleteOk.toString());
logger.info("Deleted queue " + queueName + " used for initial load: " + deleteOk.toString());
}
}
@@ -498,25 +548,12 @@ OperationExecutionListener<S>, HasReplicable<S, O> {
}
}
/**
* {@link #broadcastOperation(OperationWithResult) Broadcasts} the <code>operation</code> to all registered
* replicas by publishing it to the fan-out exchange.
*/
@Override
public <T> void executed(OperationWithResult<S, T> operation) {
try {
broadcastOperation(operation);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected void registerReplicaUuidForMaster(String uuid, ReplicationMasterDescriptor master) {
replicaUUIDs.put(master, uuid);
}
@Override
public Map<Class<? extends OperationWithResult<S, ?>>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor) {
public Map<Class<? extends OperationWithResult<?, ?>>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor) {
return replicationInstancesManager.getStatistics(replicaDescriptor);
}
@@ -52,19 +52,20 @@ public class ReplicationServlet extends AbstractHttpServlet {
*/
private static final int INITIAL_LOAD_PACKAGE_SIZE = 1024*1024;
private ServiceTracker<ReplicationService<?>, ReplicationService<?>> replicationServiceTracker;
public static final String REPLICA_IDS_AS_STRINGS_COMMA_SEPARATED = "replicaIdsAsStringsCommaSeparated";
private final ServiceTracker<ReplicationService, ReplicationService> replicationServiceTracker;
private ServiceTracker<Replicable<?, ?>, Replicable<?, ?>> replicableServiceTracker;
private final OSGiReplicableTracker replicablesProvider;
public ReplicationServlet() throws Exception {
BundleContext context = Activator.getDefaultContext();
replicationServiceTracker = new ServiceTracker<ReplicationService<?>, ReplicationService<?>>(context, ReplicationService.class.getName(), null);
replicablesProvider = new OSGiReplicableTracker(context);
replicationServiceTracker = new ServiceTracker<ReplicationService, ReplicationService>(context, ReplicationService.class.getName(), null);
replicationServiceTracker.open();
replicableServiceTracker = new ServiceTracker<Replicable<?, ?>, Replicable<?, ?>>(context, Replicable.class.getName(), null);
replicableServiceTracker.open();
}
protected ReplicationService<?> getReplicationService() {
protected ReplicationService getReplicationService() {
return replicationServiceTracker.getService();
}
@@ -87,6 +88,7 @@ public class ReplicationServlet extends AbstractHttpServlet {
deregisterClientWithReplicationService(req, resp);
break;
case INITIAL_LOAD:
String[] replicableIdsAsStrings = req.getParameter(REPLICA_IDS_AS_STRINGS_COMMA_SEPARATED).split(",");
Channel channel = getReplicationService().createMasterChannel();
RabbitOutputStream ros = new RabbitOutputStream(INITIAL_LOAD_PACKAGE_SIZE, channel,
/* queueName */ "initialLoad-for-"+req.getRemoteHost()+"@"+new Date()+"-"+UUID.randomUUID(),
@@ -98,15 +100,18 @@ public class ReplicationServlet extends AbstractHttpServlet {
ros, /* log every megabyte */1024l * 1024l, Level.INFO,
"HTTP output for initial load for " + req.getRemoteHost());
final GZIPOutputStream gzipOutputStream = new GZIPOutputStream(countingOutputStream);
ObjectOutputStream oos = new ObjectOutputStream(gzipOutputStream);
try {
getReplicable().serializeForInitialReplication(oos);
gzipOutputStream.finish();
} catch (Exception e) {
logger.info("Error trying to serialize initial load for replication: "+e.getMessage());
logger.log(Level.SEVERE, "doGet", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
e.printStackTrace(resp.getWriter());
for (String replicableIdAsString : replicableIdsAsStrings) {
Replicable<?, ?> replicable = replicablesProvider.getReplicable(replicableIdAsString);
ObjectOutputStream oos = new ObjectOutputStream(gzipOutputStream);
try {
replicable.serializeForInitialReplication(oos);
gzipOutputStream.finish();
} catch (Exception e) {
logger.info("Error trying to serialize initial load for replication: " + e.getMessage());
logger.log(Level.SEVERE, "doGet", e);
resp.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
e.printStackTrace(resp.getWriter());
}
}
countingOutputStream.close();
break;
@@ -116,10 +121,6 @@ public class ReplicationServlet extends AbstractHttpServlet {
}
}
private Replicable<?, ?> getReplicable() {
return replicableServiceTracker.getService();
}
private void deregisterClientWithReplicationService(HttpServletRequest req, HttpServletResponse resp) throws IOException {
ReplicaDescriptor replica = getReplicaDescriptor(req);
getReplicationService().unregisterReplica(replica);
@@ -1,13 +1,16 @@
package com.sap.sse.replication.impl;
import java.io.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
@@ -15,22 +18,35 @@ import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import java.util.zip.GZIPInputStream;
import com.rabbitmq.client.ConsumerCancelledException;
import com.rabbitmq.client.QueueingConsumer;
import com.rabbitmq.client.QueueingConsumer.Delivery;
import com.rabbitmq.client.ShutdownSignalException;
import com.sap.sse.common.Named;
import com.sap.sse.common.Util;
import com.sap.sse.common.Util.Pair;
import com.sap.sse.replication.OperationWithResult;
import com.sap.sse.replication.Replicable;
import com.sap.sse.replication.ReplicablesProvider;
import com.sap.sse.replication.ReplicationMasterDescriptor;
/**
* Receives {@link OperationWithResult} objects from a message queue and {@link Replicable#apply(OperationWithResult)
* applies} them to the {@link Replicable} passed to this replicator at construction. When started in suspended mode,
* messages received will be turned into {@link OperationWithResult} objects and then queued until
* {@link #setSuspended(boolean) setSuspended(false)} is invoked which applies all queued operations before applying the
* ones received later.
* applies} them to the {@link Replicable} objects that can be found through the {@link ReplicablesProvider} passed to
* this replicator at construction, based on the {@link Replicable#getId() replicable's ID} which is used in the stream
* to prefix the operations.
* <p>
*
* The corresponding writer for this protocol is implemented by
* {@link ReplicationServiceImpl#broadcastOperation(OperationWithResult, Replicable)}.<p>
*
* When started in suspended mode, messages received will be turned into {@link OperationWithResult} objects and then
* queued until {@link #setSuspended(boolean) setSuspended(false)} is invoked which applies all queued operations before
* applying the ones received later.
* <p>
*
* The receiver takes care of synchronizing receiving, suspending/resuming and queuing. Waiters are notified whenever
@@ -39,15 +55,20 @@ import com.sap.sse.replication.ReplicationMasterDescriptor;
* @author Axel Uhl (d043530)
*
*/
public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runnable {
public class Replicator implements Runnable {
private final static Logger logger = Logger.getLogger(Replicator.class.getName());
private static final long CHECK_INTERVAL_MILLIS = 2000; // how long (milliseconds) to pause before checking connection again
private static final int CHECK_COUNT = 150; // how long to check, value is CHECK_INTERVAL second steps
private final ReplicationMasterDescriptor master;
private final HasReplicable<S, O> replicableProvider;
private final List<OperationWithResult<S, ?>> queue;
private final ReplicablesProvider replicableProvider;
/**
* Keys are the {@link Replicable}s' IDs as string; values are the operation queues for the replicable identified by the
* key.
*/
private final Map<String, List<Pair<String, OperationWithResult<?, ?>>>> queueByReplicableIdAsString;
private QueueingConsumer consumer;
@@ -90,8 +111,8 @@ public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runna
* @param consumer
* the RabbitMQ consumer from which to load messages
*/
public Replicator(ReplicationMasterDescriptor master, HasReplicable<S, O> replicableProvider, boolean startSuspended, QueueingConsumer consumer) {
this.queue = new ArrayList<OperationWithResult<S, ?>>();
public Replicator(ReplicationMasterDescriptor master, ReplicablesProvider replicableProvider, boolean startSuspended, QueueingConsumer consumer) {
this.queueByReplicableIdAsString = new HashMap<>();
this.master = master;
this.replicableProvider = replicableProvider;
this.suspended = startSuspended;
@@ -136,31 +157,37 @@ public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runna
}
byte[] bytesFromMessage = delivery.getBody();
checksPerformed = 0;
// Set this object's class's class loader as context for de-serialization so that all exported classes
// Set the replicable's class's class loader as context for deserialization so that all exported classes
// of all required bundles/packages can be deserialized at least
Replicable<S, O> replicable = replicableProvider.getReplicable();
Thread.currentThread().setContextClassLoader(replicable.getClass().getClassLoader());
ObjectInputStream ois = replicable
.createObjectInputStreamResolvingAgainstCache(
new GZIPInputStream(new ByteArrayInputStream(bytesFromMessage)));
int operationsInMessage = 0;
try {
while (true) {
byte[] serializedOperation = (byte[]) ois.readObject();
ObjectInputStream operationOIS = replicable
.createObjectInputStreamResolvingAgainstCache(new ByteArrayInputStream(serializedOperation));
@SuppressWarnings("unchecked")
OperationWithResult<S, ?> operation = (OperationWithResult<S, ?>) operationOIS.readObject();
operationCount++;
operationsInMessage++;
if (operationCount % 10000l == 0) {
logger.info("Received " + operationCount + " operations so far");
final GZIPInputStream uncompressedInputStream = new GZIPInputStream(new ByteArrayInputStream(bytesFromMessage));
String replicableIdAsString = new DataInputStream(uncompressedInputStream).readUTF();
Replicable<?, ?> replicable = replicableProvider.getReplicable(replicableIdAsString);
if (replicable != null) {
Thread.currentThread().setContextClassLoader(replicable.getClass().getClassLoader());
ObjectInputStream ois = new ObjectInputStream(uncompressedInputStream); // no special stream required; only reading a generic byte[]
int operationsInMessage = 0;
try {
while (true) {
byte[] serializedOperation = (byte[]) ois.readObject();
readOperationAndApplyOrQueueIt(replicable, serializedOperation);
operationCount++;
operationsInMessage++;
if (operationCount % 10000l == 0) {
logger.info("Received " + operationCount + " operations so far");
}
}
applyOrQueue(operation);
} catch (EOFException eof) {
logger.fine("Reached EOF on replication message after having read " + operationsInMessage
+ " operations");
// reached EOF; expected
}
} catch (EOFException eof) {
logger.fine("Reached EOF on replication message after having read "+operationsInMessage+" operations");
// reached EOF; expected
} else {
// otherwise, we don't know the replicable and simply drop the message with all the operations for the unknown recipient
Stream<Named> sn = StreamSupport.stream(replicableProvider.getReplicables().spliterator(), /* parallel */ false)
.map(r->(() -> r.getId().toString()));
logger.warning("received replication message for replicable with ID "+replicableIdAsString+" which is unknnown by this replicator "+
"which only knows "+
Util.join(", ", sn::iterator));
}
} catch (ConsumerCancelledException cce) {
logger.info("Consumer has been shut down properly.");
@@ -217,27 +244,38 @@ public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runna
logger.info("Stopped replicator thread. This server will no longer receive events from a master.");
}
public synchronized boolean isQueueEmpty() {
return queue.isEmpty();
private <S, O extends OperationWithResult<S, ?>> void readOperationAndApplyOrQueueIt(Replicable<S, O> replicable,
byte[] serializedOperation) throws ClassNotFoundException, IOException {
OperationWithResult<S, ?> operation = replicable.readOperation(new ByteArrayInputStream(serializedOperation));
applyOrQueue(operation, replicable);
}
/**
* If the replicator is currently {@link #suspended}, the <code>operation</code> is queued, otherwise immediately applied to
* the receiving replica.
* If the replicator is currently {@link #suspended}, the <code>operation</code> is queued, otherwise immediately
* applied to the receiving replica.
*
* @param replicable
* the replicable to which to apply or for which to queue the operation
*/
private synchronized void applyOrQueue(OperationWithResult<S, ?> operation) {
private synchronized <S, O extends OperationWithResult<S, ?>> void applyOrQueue(OperationWithResult<S, ?> operation, Replicable<S, O> replicable) {
if (suspended) {
queue(operation);
queue(operation, replicable);
} else {
apply(operation);
apply(operation, replicable);
}
}
private synchronized void apply(final OperationWithResult<S, ?> operation) {
private synchronized <S, O extends OperationWithResult<S, ?>> void apply(final OperationWithResult<S, ?> operation, String replicableIdAsString) {
@SuppressWarnings("unchecked")
Replicable<S, O> replicable = (Replicable<S, O>) replicableProvider.getReplicable(replicableIdAsString);
apply(operation, replicable);
}
private synchronized <S, O extends OperationWithResult<S, ?>> void apply(final OperationWithResult<S, ?> operation, Replicable<S, O> replicable) {
Runnable runnable = new Runnable() {
@Override
public void run() {
replicableProvider.getReplicable().apply(operation);
replicable.apply(operation);
}
};
if (operation.requiresSynchronousExecution()) {
@@ -247,11 +285,12 @@ public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runna
}
}
private synchronized void queue(OperationWithResult<S, ?> operation) {
private synchronized void queue(OperationWithResult<?, ?> operation, Replicable<?, ?> replicable) {
List<Pair<String, OperationWithResult<?, ?>>> queue = queueByReplicableIdAsString.get(replicable.getId().toString());
if (queue.isEmpty()) {
notifyAll();
}
queue.add(operation);
queue.add(new Pair<String, OperationWithResult<?, ?>>(replicable.getId().toString(), operation));
assert !queue.isEmpty();
}
@@ -259,22 +298,26 @@ public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runna
if (this.suspended != suspended) {
this.suspended = suspended;
if (!this.suspended) {
applyQueue();
applyQueues();
}
}
}
private synchronized void applyQueue() {
for (Iterator<OperationWithResult<S, ?>> i=queue.iterator(); i.hasNext(); ) {
OperationWithResult<S, ?> operation = i.next();
i.remove();
try {
apply(operation);
} catch (Exception e) {
logger.log(Level.SEVERE, "Error applying queued, replicated operation "+operation+". Continuing with next queued operation.", e);
private synchronized void applyQueues() {
for (Entry<String, List<Pair<String, OperationWithResult<?, ?>>>> r : queueByReplicableIdAsString.entrySet()) {
final List<Pair<String, OperationWithResult<?, ?>>> queue = r.getValue();
for (Iterator<Pair<String, OperationWithResult<?, ?>>> i = queue.iterator(); i.hasNext();) {
Pair<String, OperationWithResult<?, ?>> replicableIdAsStringAndOperation = i.next();
i.remove();
try {
apply(replicableIdAsStringAndOperation.getB(), replicableIdAsStringAndOperation.getA());
} catch (Exception e) {
logger.log(Level.SEVERE, "Error applying queued, replicated operation "
+ replicableIdAsStringAndOperation + ". Continuing with next queued operation.", e);
}
}
assert queue.isEmpty();
}
assert queue.isEmpty();
notifyAll();
}
@@ -285,7 +328,7 @@ public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runna
public synchronized void stop() {
if (isSuspended()) {
/* make sure to apply everything in queue before stopping this thread */
applyQueue();
applyQueues();
}
logger.info("Signaled Replicator thread to stop asap.");
stopped = true;
@@ -298,7 +341,8 @@ public class Replicator<S, O extends OperationWithResult<S, ?>> implements Runna
@Override
public String toString() {
return "Replicator for master "+master+", queue size: "+queue.size();
long queueSize = queueByReplicableIdAsString.values().stream().mapToLong(l->l.size()).sum();
return "Replicator for master "+master+", queue size: "+queueSize;
}
}