diff --git a/java/com.sap.sailing.domain.test/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain.test/META-INF/MANIFEST.MF index 11ff5b2ec1a..ed6bb411bdc 100644 --- a/java/com.sap.sailing.domain.test/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain.test/META-INF/MANIFEST.MF @@ -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 diff --git a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF index 1b0da8d8d32..ca5fb94e6b8 100644 --- a/java/com.sap.sailing.domain/META-INF/MANIFEST.MF +++ b/java/com.sap.sailing.domain/META-INF/MANIFEST.MF @@ -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 diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/DomainFactory.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/DomainFactory.java index 5c7bd10c123..d24eacc32eb 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/DomainFactory.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/DomainFactory.java @@ -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 createObjectInputStreamResolvingAgainstThisFactory(InputStream inputStream) throws IOException; ScoringScheme createScoringScheme(ScoringSchemeType scoringSchemeType); diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/ObjectInputStreamResolvingAgainstDomainFactory.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/ObjectInputStreamResolvingAgainstDomainFactory.java deleted file mode 100755 index f99cefceae6..00000000000 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/ObjectInputStreamResolvingAgainstDomainFactory.java +++ /dev/null @@ -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. - *

- * - * 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 castResult = ((IsManagedByCache) o).resolve(getDomainFactory()); - result = castResult; - } else { - result = o; - } - return result; - } -} diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java index 85b5c593805..898f9a24e6f 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/DomainFactoryImpl.java @@ -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 createObjectInputStreamResolvingAgainstThisFactory(InputStream inputStream) throws IOException { return new ObjectInputStreamResolvingAgainstDomainFactoryImpl(inputStream, this); } diff --git a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ObjectInputStreamResolvingAgainstDomainFactoryImpl.java b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ObjectInputStreamResolvingAgainstDomainFactoryImpl.java index ec42320df84..dc24656d2c4 100755 --- a/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ObjectInputStreamResolvingAgainstDomainFactoryImpl.java +++ b/java/com.sap.sailing.domain/src/com/sap/sailing/domain/base/impl/ObjectInputStreamResolvingAgainstDomainFactoryImpl.java @@ -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 { ObjectInputStreamResolvingAgainstDomainFactoryImpl(InputStream in, DomainFactory domainFactory) throws IOException { super(in, domainFactory); diff --git a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java index 71105220035..e4edc14689a 100755 --- a/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java +++ b/java/com.sap.sailing.gwt.ui.test/src/com/sap/sailing/gwt/ui/test/SailingServiceImplMock.java @@ -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> createAndOpenReplicationServiceTracker( + protected ServiceTracker createAndOpenReplicationServiceTracker( BundleContext context) { @SuppressWarnings("unchecked") - ServiceTracker, ReplicationService> result = mock(ServiceTracker.class); + ServiceTracker result = mock(ServiceTracker.class); try { - final ReplicationServiceImpl> replicationService = - new ReplicationServiceImpl>("test exchange", "localhost", 0, - new ReplicationInstancesManager()) { - @Override - protected ServiceTracker>, Replicable>> getReplicableTracker() { - @SuppressWarnings("unchecked") - ServiceTracker>, Replicable>> result = - (ServiceTracker>, Replicable>>) 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> getReplicables() { + return Collections.>singleton(replicable); + } + }); when(result.getService()).thenReturn(replicationService); } catch (IOException e) { throw new RuntimeException(); diff --git a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java index 71f187ccf07..40aed307ac0 100755 --- a/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java +++ b/java/com.sap.sailing.gwt.ui/src/main/java/com/sap/sailing/gwt/ui/server/SailingServiceImpl.java @@ -419,7 +419,7 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S private final ServiceTracker racingEventServiceTracker; - private final ServiceTracker, ReplicationService> replicationServiceTracker; + private final ServiceTracker replicationServiceTracker; private final ServiceTracker resultUrlRegistryServiceTracker; @@ -662,10 +662,10 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S return new ScoreCorrectionProviderDTO(scoreCorrectionProvider.getName(), hasResultsForBoatClassFromDateByEventName); } - protected ServiceTracker, ReplicationService> createAndOpenReplicationServiceTracker( + protected ServiceTracker createAndOpenReplicationServiceTracker( BundleContext context) { - ServiceTracker, ReplicationService> result = - new ServiceTracker, ReplicationService>( + ServiceTracker result = + new ServiceTracker( 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 getReplicationService() { + private ReplicationService getReplicationService() { return replicationServiceTracker.getService(); } @@ -3193,12 +3193,12 @@ public class SailingServiceImpl extends ProxiedRemoteServiceServlet implements S @Override public ReplicationStateDTO getReplicaInfo() { - ReplicationService service = getReplicationService(); + ReplicationService service = getReplicationService(); Set replicaDTOs = new HashSet(); for (ReplicaDescriptor replicaDescriptor : service.getReplicaInfo()) { - final Map>, Integer> statistics = service.getStatistics(replicaDescriptor); + final Map>, Integer> statistics = service.getStatistics(replicaDescriptor); Map replicationCountByOperationClassName = new HashMap(); - for (Entry>, Integer> e : statistics.entrySet()) { + for (Entry>, Integer> e : statistics.entrySet()) { replicationCountByOperationClassName.put(e.getKey().getName(), e.getValue()); } replicaDTOs.add(new ReplicaDTO(replicaDescriptor.getIpAddress().getHostName(), replicaDescriptor diff --git a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java index d637d193f7c..dfc4629532e 100755 --- a/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java +++ b/java/com.sap.sailing.server.replication.test/src/com/sap/sailing/server/replication/test/AbstractServerReplicationTest.java @@ -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> 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> { + 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 masterReplicationService; + private final ReplicationService masterReplicationService; private final ReplicationMasterDescriptor masterDescriptor; public ReplicationServiceTestImpl(String exchangeName, String exchangeHost, DomainFactory resolveAgainst, - ReplicationInstancesManager replicationInstancesManager, ReplicaDescriptor replicaDescriptor, - RacingEventService replica, RacingEventService master, ReplicationService 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> 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(); } } } diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java index 10caee74a56..f6b71219109 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/RacingEventService.java @@ -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.

+ * 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. + *

* * 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.

+ * {@link #addRegatta(URL, URI, URI, WindStore, long)} call will have no effect, even if a different {@link WindStore} + * is requested. + *

* - * 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. + *

+ * + * 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, - Replicable> { + ReplicableWithObjectInputStream> { @Override Regatta getRegatta(RegattaName regattaName); diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java index 8ee5c741a9b..ea489b80ec6 100755 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/impl/RacingEventServiceImpl.java @@ -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 { diff --git a/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java b/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java index ec585d60e16..ae0912fd725 100644 --- a/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java +++ b/java/com.sap.sailing.server/src/com/sap/sailing/server/masterdata/MasterDataImporter.java @@ -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 objectInputStream = racingEventService.getBaseDomainFactory() .createObjectInputStreamResolvingAgainstThisFactory(inputStream); racingEventService .createOrUpdateDataImportProgressWithReplication(importOperationId, 0.03, "Reading Data", 0.5); diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java index 482e1c83e95..04dec321ac4 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/Replicable.java @@ -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 toState argument in the {@link Operation#applyTo(Object)} method calls.

+ * 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 toState argument in the {@link Operation#applyTo(Object)} method calls. + *

* - * 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 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 transient + * 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. + *

* - * 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.

+ * of that type. + *

* * 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 the type of state to which the operations are applied; usually this will be set to the implementing subclass - * @param type of operation that the replicable accepts + * @param + * the type of state to which the operations are applied; usually this will be set to the implementing + * subclass + * @param + * type of operation that the replicable accepts * * @author Axel Uhl (D043530) * @@ -51,12 +65,6 @@ public interface Replicable> 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> extends With */ T apply(OperationWithResult operation); + /** + * An operation execution listener must be able to process notifications of operations being executed that have + * type S or any more specific type. The listener can achieve this also by accepting any type more general than + * S. + */ void addOperationExecutionListener(OperationExecutionListener listener); void removeOperationExecutionListener(OperationExecutionListener listener); @@ -80,27 +93,32 @@ public interface Replicable> 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. *

* - * 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.

- * * Caution: 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 oos 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 os 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; } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicablesProvider.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicablesProvider.java new file mode 100755 index 00000000000..4c3e765ceac --- /dev/null +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicablesProvider.java @@ -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> 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 null if no such replicable can be found; the replicable otherwise + */ + Replicable getReplicable(String replicableIdAsString); + + void addReplicableLifeCycleListener(ReplicableLifeCycleListener listener); + + void removeReplicableLifeCycleListener(ReplicableLifeCycleListener listener); +} diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java index eabe6fd3a61..9d865f089c2 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationMasterDescriptor.java @@ -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> replicables) throws MalformedURLException; int getMessagingPort(); diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java index 0c974d1d99a..d123e2e6deb 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/ReplicationService.java @@ -16,7 +16,7 @@ import com.sap.sse.replication.impl.ReplicationServlet; * * @param the type representing the master's state */ -public interface ReplicationService { +public interface ReplicationService { static String SAILING_SERVER_REPLICATION_TOPIC = "SailingServerReplicationTopic"; /** @@ -57,7 +57,7 @@ public interface ReplicationService { * 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>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor); + Map>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor); double getAverageNumberOfOperationsPerMessage(ReplicaDescriptor replicaDescriptor); diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/AbstractReplicablesProvider.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/AbstractReplicablesProvider.java new file mode 100755 index 00000000000..5bb73bf9a98 --- /dev/null +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/AbstractReplicablesProvider.java @@ -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 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 replicableIdAsString. + * + * @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); + } +} diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java index 914953f391b..a4e83b02162 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Activator.java @@ -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..."); diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/HasReplicable.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/HasReplicable.java deleted file mode 100755 index 34049861d3d..00000000000 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/HasReplicable.java +++ /dev/null @@ -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> { - Replicable getReplicable(); -} diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/OSGiReplicableTracker.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/OSGiReplicableTracker.java new file mode 100755 index 00000000000..0b12589c218 --- /dev/null +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/OSGiReplicableTracker.java @@ -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> serviceTracker; + private final Map>> serviceReferenceByIdAsString; + private final Map>, 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>( + bundleContext, Replicable.class.getName(), new ServiceTrackerCustomizer, Replicable>() { + @Override + public Replicable addingService(ServiceReference> 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> reference, + Replicable service) { + serviceReferenceByIdAsString.remove(idAsStringByServiceReference.get(reference)); + idAsStringByServiceReference.remove(reference); + } + + @Override public void modifiedService(ServiceReference> reference, Replicable service) {} + }); + serviceTracker.open(); + } + + @Override + public Iterable> getReplicables() { + return Arrays.asList(serviceTracker.getServices(new Replicable[0])); + } + + @Override + public Replicable getReplicable(String replicableIdAsString) { + return bundleContext.getService(serviceReferenceByIdAsString.get(replicableIdAsString)); + } +} \ No newline at end of file diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ObjectInputStreamResolvingAgainstCache.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ObjectInputStreamResolvingAgainstCache.java index 1b1cb3d372b..fda698e4c91 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ObjectInputStreamResolvingAgainstCache.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ObjectInputStreamResolvingAgainstCache.java @@ -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 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. *

* - * 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. + *

* - * @param 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 abstract. + * + * @param + * the cache type used for resolving objects * * @author Axel Uhl (D043530) * diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java new file mode 100755 index 00000000000..c2a1aa855d3 --- /dev/null +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicableWithObjectInputStream.java @@ -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> extends Replicable { + /** + * 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 os 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)); + } + +} diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java index 390e68be032..f9b2517cc32 100644 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationInstancesManager.java @@ -18,7 +18,7 @@ import com.sap.sse.replication.ReplicationMasterDescriptor; * * @param the state to which replication operations are applied */ -public class ReplicationInstancesManager { +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 { */ private Set replicaDescriptors; - private Map>, Integer>> replicationCounts; + private Map>, Integer>> replicationCounts; /** * Used to calculate the average number of operations per message sent/received for each replica. @@ -54,7 +54,7 @@ public class ReplicationInstancesManager { public ReplicationInstancesManager() { replicaDescriptors = new HashSet(); - replicationCounts = new HashMap>,Integer>>(); + replicationCounts = new HashMap>,Integer>>(); totalMessageCount = new HashMap<>(); totalNumberOfOperations = new HashMap<>(); totalQueueMessagesRawSizeInBytes = new HashMap(); @@ -80,7 +80,7 @@ public class ReplicationInstancesManager { public void registerReplica(ReplicaDescriptor replica) { replicaDescriptors.add(replica); - replicationCounts.put(replica, new HashMap>, Integer>()); + replicationCounts.put(replica, new HashMap>, Integer>()); } public void unregisterReplica(ReplicaDescriptor replica) { @@ -96,15 +96,15 @@ public class ReplicationInstancesManager { */ public void log(List> classes, long sizeOfQueueMessageInBytes) { for (ReplicaDescriptor replica : getReplicaDescriptors()) { - Map>, Integer> counts = replicationCounts.get(replica); + Map>, Integer> counts = replicationCounts.get(replica); if (counts == null) { - counts = new HashMap>, Integer>(); + counts = new HashMap>, Integer>(); replicationCounts.put(replica, counts); } for (Class replicatedOperationClass : classes) { // safe because replicatedOperation is declared of type RacingEventserviceOperation @SuppressWarnings("unchecked") - Class> operationClass = (Class>) replicatedOperationClass; + Class> operationClass = (Class>) replicatedOperationClass; Integer count = counts.get(operationClass); if (count == null) { count = 0; @@ -130,7 +130,7 @@ public class ReplicationInstancesManager { } } - public Map>, Integer> getStatistics(ReplicaDescriptor replica) { + public Map>, Integer> getStatistics(ReplicaDescriptor replica) { return replicationCounts.get(replica); } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java index fe403ca9bf8..1d2b9745d9d 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationMasterDescriptorImpl.java @@ -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> replicables) throws MalformedURLException { return new URL("http", getHostname(), servletPort, REPLICATION_SERVLET + "?" + ReplicationServlet.ACTION + "=" + ReplicationServlet.Action.INITIAL_LOAD.name()); } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceExecutionListener.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceExecutionListener.java new file mode 100755 index 00000000000..3e17d3d1c4a --- /dev/null +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceExecutionListener.java @@ -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) broadcast} the operation to any interested + * replica. + * + * @author Axel Uhl (D043530) + * + * @param + */ +public class ReplicationServiceExecutionListener implements OperationExecutionListener { + private final ReplicationServiceImpl replicationService; + private final Replicable replicable; + + /** + * {@link Replicable#addOperationExecutionListener(OperationExecutionListener) Adds} this new listener to the replicable + */ + protected ReplicationServiceExecutionListener(ReplicationServiceImpl replicationService, Replicable replicable) { + super(); + this.replicationService = replicationService; + this.replicable = replicable; + this.replicable.addOperationExecutionListener(this); + } + + /** + * {@link #broadcastOperation(OperationWithResult, Replicable) Broadcasts} the operation to all registered + * replicas by publishing it to the fan-out exchange. + */ + @Override + public void executed(OperationWithResult operation) { + try { + replicationService.broadcastOperation(operation, replicable); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public void unsubscribe() { + this.replicable.removeOperationExecutionListener(this); + } +} diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java index f6d41bd0185..b465364991b 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServiceImpl.java @@ -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. *

* - * 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. + *

+ * + * The exchange name and connectivity information for the message queuing system are provided to this service during + * construction. + *

+ * + * 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> implements ReplicationService, -OperationExecutionListener, HasReplicable { +public class ReplicationServiceImpl implements ReplicationService { private static final Logger logger = Logger.getLogger(ReplicationServiceImpl.class.getName()); - private final ReplicationInstancesManager replicationInstancesManager; + private final ReplicationInstancesManager replicationInstancesManager; - private final ServiceTracker, Replicable> replicableTracker; - - private final Replicable localService; + private final ReplicablesProvider replicablesProvider; /** * null, if this instance is not currently replicating from some master; the master's descriptor otherwise @@ -102,7 +115,9 @@ OperationExecutionListener, HasReplicable { * 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 replicator; + private Replicator replicator; + + private final Set> executionListeners; private Thread replicatorThread; @@ -120,6 +135,17 @@ OperationExecutionListener, HasReplicable { * 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, HasReplicable { */ 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 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 replicationInstancesManager, - Replicable 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(); - 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 replicationInstancesManager, Replicable localService) throws IOException { - this(exchangeName, exchangeHost, 0, replicationInstancesManager, localService, /* create ReplicableServiceTracker */ false); - } - - protected ServiceTracker, Replicable> getReplicableTracker() { - return new ServiceTracker, Replicable>( + protected ServiceTracker, Replicable> getReplicableTracker() { + return new ServiceTracker, Replicable>( Activator.getDefaultContext(), Replicable.class.getName(), null); } @@ -247,15 +274,8 @@ OperationExecutionListener, HasReplicable { return result; } - @Override - public Replicable getReplicable() { - Replicable result; - if (localService != null) { - result = localService; - } else { - result = replicableTracker.getService(); - } - return result; + private Iterable> getReplicables() { + return replicablesProvider.getReplicables(); } @Override @@ -273,7 +293,14 @@ OperationExecutionListener, HasReplicable { } private void addAsListenerToReplicables() { - getReplicable().addOperationExecutionListener(this); + for (Replicable replicable : getReplicables()) { + addNewOperationExecutionListener(replicable); + } + } + + private void addNewOperationExecutionListener(Replicable replicable) { + final ReplicationServiceExecutionListener listener = new ReplicationServiceExecutionListener(this, replicable); + executionListeners.add(listener); } @Override @@ -292,29 +319,41 @@ OperationExecutionListener, HasReplicable { } 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 operation) throws IOException { + > void broadcastOperation(OperationWithResult operation, Replicable 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, HasReplicable { classesOfOperationsToSend = outboundBufferClasses; doSend = true; outboundBuffer = null; + outboundBufferReplicableIdAsString = null; outboundObjectBuffer = null; outboundBufferClasses = null; } else { @@ -407,6 +447,11 @@ OperationExecutionListener, HasReplicable { 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, HasReplicable { throw ex; } logger.info("Connection to exchange successful."); - URL initialLoadURL = master.getInitialLoadURL(); + final Iterable> replicables = replicablesProvider.getReplicables(); + URL initialLoadURL = master.getInitialLoadURL(replicables); logger.info("Initial load URL is "+initialLoadURL); - replicator = new Replicator(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, HasReplicable { } } - /** - * {@link #broadcastOperation(OperationWithResult) Broadcasts} the operation to all registered - * replicas by publishing it to the fan-out exchange. - */ - @Override - public void executed(OperationWithResult 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>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor) { + public Map>, Integer> getStatistics(ReplicaDescriptor replicaDescriptor) { return replicationInstancesManager.getStatistics(replicaDescriptor); } diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java index 6fa8e052ad7..fe65fc79478 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/ReplicationServlet.java @@ -52,19 +52,20 @@ public class ReplicationServlet extends AbstractHttpServlet { */ private static final int INITIAL_LOAD_PACKAGE_SIZE = 1024*1024; - private ServiceTracker, ReplicationService> replicationServiceTracker; + public static final String REPLICA_IDS_AS_STRINGS_COMMA_SEPARATED = "replicaIdsAsStringsCommaSeparated"; + + private final ServiceTracker replicationServiceTracker; - private ServiceTracker, Replicable> replicableServiceTracker; + private final OSGiReplicableTracker replicablesProvider; public ReplicationServlet() throws Exception { BundleContext context = Activator.getDefaultContext(); - replicationServiceTracker = new ServiceTracker, ReplicationService>(context, ReplicationService.class.getName(), null); + replicablesProvider = new OSGiReplicableTracker(context); + replicationServiceTracker = new ServiceTracker(context, ReplicationService.class.getName(), null); replicationServiceTracker.open(); - replicableServiceTracker = new ServiceTracker, 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); diff --git a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Replicator.java b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Replicator.java index 557e8fa8683..6d3411e064d 100755 --- a/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Replicator.java +++ b/java/com.sap.sse.replication/src/com/sap/sse/replication/impl/Replicator.java @@ -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. + *

+ * + * The corresponding writer for this protocol is implemented by + * {@link ReplicationServiceImpl#broadcastOperation(OperationWithResult, Replicable)}.

+ * + * 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. *

* * 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> 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 replicableProvider; - private final List> 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>>> queueByReplicableIdAsString; private QueueingConsumer consumer; @@ -90,8 +111,8 @@ public class Replicator> implements Runna * @param consumer * the RabbitMQ consumer from which to load messages */ - public Replicator(ReplicationMasterDescriptor master, HasReplicable replicableProvider, boolean startSuspended, QueueingConsumer consumer) { - this.queue = new ArrayList>(); + 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> 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 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 operation = (OperationWithResult) 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 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> 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 > void readOperationAndApplyOrQueueIt(Replicable replicable, + byte[] serializedOperation) throws ClassNotFoundException, IOException { + OperationWithResult operation = replicable.readOperation(new ByteArrayInputStream(serializedOperation)); + applyOrQueue(operation, replicable); } /** - * If the replicator is currently {@link #suspended}, the operation is queued, otherwise immediately applied to - * the receiving replica. + * If the replicator is currently {@link #suspended}, the operation 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 operation) { + private synchronized > void applyOrQueue(OperationWithResult operation, Replicable replicable) { if (suspended) { - queue(operation); + queue(operation, replicable); } else { - apply(operation); + apply(operation, replicable); } } - private synchronized void apply(final OperationWithResult operation) { + private synchronized > void apply(final OperationWithResult operation, String replicableIdAsString) { + @SuppressWarnings("unchecked") + Replicable replicable = (Replicable) replicableProvider.getReplicable(replicableIdAsString); + apply(operation, replicable); + } + + private synchronized > void apply(final OperationWithResult operation, Replicable 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> implements Runna } } - private synchronized void queue(OperationWithResult operation) { + private synchronized void queue(OperationWithResult operation, Replicable replicable) { + List>> queue = queueByReplicableIdAsString.get(replicable.getId().toString()); if (queue.isEmpty()) { notifyAll(); } - queue.add(operation); + queue.add(new Pair>(replicable.getId().toString(), operation)); assert !queue.isEmpty(); } @@ -259,22 +298,26 @@ public class Replicator> implements Runna if (this.suspended != suspended) { this.suspended = suspended; if (!this.suspended) { - applyQueue(); + applyQueues(); } } } - private synchronized void applyQueue() { - for (Iterator> i=queue.iterator(); i.hasNext(); ) { - OperationWithResult 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>>> r : queueByReplicableIdAsString.entrySet()) { + final List>> queue = r.getValue(); + for (Iterator>> i = queue.iterator(); i.hasNext();) { + Pair> 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> 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> 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; } }