diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 000000000..9e3198100 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @splitio/sdk diff --git a/CHANGES.txt b/CHANGES.txt index 210c2843e..ee78d8ba1 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,8 @@ +4.8.0 (Jul 18, 2023) +- Improved streaming architecture implementation to apply feature flag updates from the notification received which is now enhanced, improving efficiency and reliability of the whole update system. +- Updated `com.google.guava` dependence to 32.0.1 for fixing a vulnerability. +- Updated SegmentFetcher for better readability. + 4.7.2 (May 16, 2023) - Updated default treatment to be control for yaml and json localhost. - Updated terminology on the SDKs codebase to be more aligned with current standard without causing a breaking change. The core change is the term split for feature flag on things like logs and javadoc comments. diff --git a/client/pom.xml b/client/pom.xml index 5913349c0..4c1afd402 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,7 +5,7 @@ io.split.client java-client-parent - 4.7.2 + 4.8.0 java-client jar @@ -155,7 +155,7 @@ com.google.guava guava - 30.0-jre + 32.0.1-jre org.slf4j diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index c0c2c0dbb..ab83f8e82 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -104,7 +104,6 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; -import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.stream.Collectors; @@ -115,8 +114,6 @@ public class SplitFactoryImpl implements SplitFactory { private final static long SSE_CONNECT_TIMEOUT = 30000; private final static long SSE_SOCKET_TIMEOUT = 70000; - private static Random RANDOM = new Random(); - private final SDKReadinessGates _gates; private final ImpressionsManager _impressionsManager; private final Evaluator _evaluator; @@ -152,7 +149,6 @@ public class SplitFactoryImpl implements SplitFactory { private final URI _eventsRootTarget; private final UniqueKeysTracker _uniqueKeysTracker; - //Constructor for standalone mode public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyntaxException { _userStorageWrapper = null; @@ -188,13 +184,14 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn ImpressionsStorage impressionsStorage = new InMemoryImpressionsStorage(config.impressionsQueueSize()); _splitCache = splitCache; _segmentCache = segmentCache; - _telemetrySynchronizer = new TelemetryInMemorySubmitter(_httpclient, URI.create(config.telemetryURL()), telemetryStorage, splitCache, segmentCache, telemetryStorage, _startTime); + _telemetrySynchronizer = new TelemetryInMemorySubmitter(_httpclient, URI.create(config.telemetryURL()), telemetryStorage, splitCache, _segmentCache, telemetryStorage, _startTime); // Segments _segmentSynchronizationTaskImp = buildSegments(config, segmentCache, splitCache); + SplitParser splitParser = new SplitParser(); // SplitFetcher - _splitFetcher = buildSplitFetcher(splitCache, splitCache); + _splitFetcher = buildSplitFetcher(splitCache, splitParser); // SplitSynchronizationTask _splitSynchronizationTask = new SplitSynchronizationTask(_splitFetcher, @@ -241,7 +238,7 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn SplitAPI splitAPI = SplitAPI.build(_httpclient, buildSSEdHttpClient(apiToken, config, _sdkMetadata)); _syncManager = SyncManagerImp.build(splitTasks, _splitFetcher, splitCache, splitAPI, - segmentCache, _gates, _telemetryStorageProducer, _telemetrySynchronizer, config); + segmentCache, _gates, _telemetryStorageProducer, _telemetrySynchronizer, config, splitParser); _syncManager.start(); // DestroyOnShutDown @@ -255,7 +252,6 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn } } - //Constructor for consumer mode protected SplitFactoryImpl(String apiToken, SplitClientConfig config, CustomStorageWrapper customStorageWrapper) throws URISyntaxException { //Variables that are not used in Consumer mode. @@ -378,7 +374,7 @@ protected SplitFactoryImpl(SplitClientConfig config) { SplitParser splitParser = new SplitParser(); - _splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, _splitCache, splitCache, _telemetryStorageProducer); + _splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCache, _telemetryStorageProducer); // SplitSynchronizationTask _splitSynchronizationTask = new SplitSynchronizationTask(_splitFetcher, splitCache, config.featuresRefreshRate(), config.getThreadFactory()); @@ -559,11 +555,10 @@ private SegmentSynchronizationTaskImp buildSegments(SplitClientConfig config, Se config.getThreadFactory()); } - private SplitFetcher buildSplitFetcher(SplitCacheConsumer splitCacheConsumer, SplitCacheProducer splitCacheProducer) throws URISyntaxException { + private SplitFetcher buildSplitFetcher(SplitCacheProducer splitCacheProducer, SplitParser splitParser) throws URISyntaxException { SplitChangeFetcher splitChangeFetcher = HttpSplitChangeFetcher.create(_httpclient, _rootTarget, _telemetryStorageProducer); - SplitParser splitParser = new SplitParser(); - return new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheConsumer, splitCacheProducer, _telemetryStorageProducer); + return new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, _telemetryStorageProducer); } private ImpressionsManagerImpl buildImpressionsManager(SplitClientConfig config, ImpressionsStorageConsumer impressionsStorageConsumer, ImpressionsStorageProducer impressionsStorageProducer) throws URISyntaxException { diff --git a/client/src/main/java/io/split/client/jmx/JmxMonitor.java b/client/src/main/java/io/split/client/jmx/JmxMonitor.java index ce2e3ff48..0acd7deda 100644 --- a/client/src/main/java/io/split/client/jmx/JmxMonitor.java +++ b/client/src/main/java/io/split/client/jmx/JmxMonitor.java @@ -130,4 +130,4 @@ private String getContextPath() { return null; } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/jmx/SplitJmxMonitor.java b/client/src/main/java/io/split/client/jmx/SplitJmxMonitor.java index 052d726ad..6f4e0522c 100644 --- a/client/src/main/java/io/split/client/jmx/SplitJmxMonitor.java +++ b/client/src/main/java/io/split/client/jmx/SplitJmxMonitor.java @@ -69,4 +69,4 @@ public String fetchDefinition(String featureName) { public boolean isKeyInSegment(String key, String segmentName) { return segmentCacheConsumer.isInSegment(segmentName, key); } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/jmx/SplitJmxMonitorMBean.java b/client/src/main/java/io/split/client/jmx/SplitJmxMonitorMBean.java index 6b492043d..94d1728bf 100644 --- a/client/src/main/java/io/split/client/jmx/SplitJmxMonitorMBean.java +++ b/client/src/main/java/io/split/client/jmx/SplitJmxMonitorMBean.java @@ -36,4 +36,4 @@ public interface SplitJmxMonitorMBean { */ boolean isKeyInSegment(String key, String segmentName); -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/utils/FeatureFlagProcessor.java b/client/src/main/java/io/split/client/utils/FeatureFlagProcessor.java new file mode 100644 index 000000000..2234f0a20 --- /dev/null +++ b/client/src/main/java/io/split/client/utils/FeatureFlagProcessor.java @@ -0,0 +1,38 @@ +package io.split.client.utils; + +import io.split.client.dtos.Split; +import io.split.client.dtos.Status; +import io.split.engine.experiments.ParsedSplit; +import io.split.engine.experiments.SplitParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class FeatureFlagProcessor { + private static final Logger _log = LoggerFactory.getLogger(FeatureFlagProcessor.class); + + public static FeatureFlagsToUpdate processFeatureFlagChanges(SplitParser splitParser, List splits) { + List toAdd = new ArrayList<>(); + List toRemove = new ArrayList<>(); + Set segments = new HashSet<>(); + for (Split split : splits) { + if (split.status != Status.ACTIVE) { + // archive. + toRemove.add(split.name); + continue; + } + ParsedSplit parsedSplit = splitParser.parse(split); + if (parsedSplit == null) { + _log.debug(String.format("We could not parse the feature flag definition for: %s", split.name)); + continue; + } + segments.addAll(parsedSplit.getSegmentsNames()); + toAdd.add(parsedSplit); + } + return new FeatureFlagsToUpdate(toAdd, toRemove, segments); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/client/utils/FeatureFlagsToUpdate.java b/client/src/main/java/io/split/client/utils/FeatureFlagsToUpdate.java new file mode 100644 index 000000000..3eb03c7b5 --- /dev/null +++ b/client/src/main/java/io/split/client/utils/FeatureFlagsToUpdate.java @@ -0,0 +1,30 @@ +package io.split.client.utils; + +import io.split.engine.experiments.ParsedSplit; + +import java.util.List; +import java.util.Set; + +public class FeatureFlagsToUpdate { + List toAdd; + List toRemove; + Set segments; + + public FeatureFlagsToUpdate(List toAdd, List toRemove, Set segments) { + this.toAdd = toAdd; + this.toRemove = toRemove; + this.segments = segments; + } + + public List getToAdd() { + return toAdd; + } + + public List getToRemove() { + return toRemove; + } + + public Set getSegments() { + return segments; + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java b/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java index 6b4e6b9f3..7bcee43a5 100644 --- a/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java +++ b/client/src/main/java/io/split/engine/common/ConsumerSynchronizer.java @@ -2,6 +2,7 @@ import io.split.client.impressions.ImpressionsManager; import io.split.client.impressions.UniqueKeysTracker; +import io.split.engine.sse.dtos.SplitKillNotification; import io.split.telemetry.synchronizer.TelemetrySyncTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +40,7 @@ public void refreshSplits(Long targetChangeNumber) { } @Override - public void localKillSplit(String featureFlagName, String defaultTreatment, long newChangeNumber) { + public void localKillSplit(SplitKillNotification splitKillNotification) { //No-Op } @@ -80,4 +81,9 @@ public void stopPeriodicDataRecording() { _telemetrySyncTask.stopScheduledTask(); _log.info("Successful shutdown of telemetry sync task"); } -} + + @Override + public void forceRefreshSegment(String segmentName) { + //No-Op + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java b/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java index a8b4cf312..e92151846 100644 --- a/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java +++ b/client/src/main/java/io/split/engine/common/LocalhostSynchronizer.java @@ -5,6 +5,7 @@ import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.segments.SegmentFetcher; import io.split.engine.segments.SegmentSynchronizationTask; +import io.split.engine.sse.dtos.SplitKillNotification; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -66,7 +67,7 @@ public void refreshSplits(Long targetChangeNumber) { } @Override - public void localKillSplit(String featureFlagName, String defaultTreatment, long newChangeNumber) { + public void localKillSplit(SplitKillNotification splitKillNotification) { //No-Op } @@ -85,4 +86,9 @@ public void startPeriodicDataRecording() { public void stopPeriodicDataRecording() { //No-Op } + + @Override + public void forceRefreshSegment(String segmentName) { + //No-Op + } } diff --git a/client/src/main/java/io/split/engine/common/PushManagerImp.java b/client/src/main/java/io/split/engine/common/PushManagerImp.java index 61fff9a1e..5fb423f0a 100644 --- a/client/src/main/java/io/split/engine/common/PushManagerImp.java +++ b/client/src/main/java/io/split/engine/common/PushManagerImp.java @@ -1,6 +1,7 @@ package io.split.engine.common; import com.google.common.annotations.VisibleForTesting; +import io.split.engine.experiments.SplitParser; import io.split.engine.sse.AuthApiClient; import io.split.engine.sse.AuthApiClientImp; import io.split.engine.sse.EventSourceClient; @@ -11,10 +12,11 @@ import io.split.engine.sse.dtos.AuthenticationResponse; import io.split.engine.sse.dtos.SegmentQueueDto; import io.split.engine.sse.workers.SegmentsWorkerImp; -import io.split.engine.sse.workers.SplitsWorker; -import io.split.engine.sse.workers.SplitsWorkerImp; +import io.split.engine.sse.workers.FeatureFlagsWorker; +import io.split.engine.sse.workers.FeatureFlagWorkerImp; import io.split.engine.sse.workers.Worker; +import io.split.storages.SplitCacheProducer; import io.split.telemetry.domain.StreamingEvent; import io.split.telemetry.domain.enums.StreamEventsEnum; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -36,7 +38,7 @@ public class PushManagerImp implements PushManager { private final AuthApiClient _authApiClient; private final EventSourceClient _eventSourceClient; - private final SplitsWorker _splitsWorker; + private final FeatureFlagsWorker _featureFlagsWorker; private final Worker _segmentWorker; private final PushStatusTracker _pushStatusTracker; @@ -48,7 +50,7 @@ public class PushManagerImp implements PushManager { @VisibleForTesting /* package private */ PushManagerImp(AuthApiClient authApiClient, EventSourceClient eventSourceClient, - SplitsWorker splitsWorker, + FeatureFlagsWorker featureFlagsWorker, Worker segmentWorker, PushStatusTracker pushStatusTracker, TelemetryRuntimeProducer telemetryRuntimeProducer, @@ -56,7 +58,7 @@ public class PushManagerImp implements PushManager { _authApiClient = checkNotNull(authApiClient); _eventSourceClient = checkNotNull(eventSourceClient); - _splitsWorker = splitsWorker; + _featureFlagsWorker = featureFlagsWorker; _segmentWorker = segmentWorker; _pushStatusTracker = pushStatusTracker; _expirationTime = new AtomicLong(); @@ -70,13 +72,15 @@ public static PushManagerImp build(Synchronizer synchronizer, SplitAPI splitAPI, LinkedBlockingQueue statusMessages, TelemetryRuntimeProducer telemetryRuntimeProducer, - ThreadFactory threadFactory) { - SplitsWorker splitsWorker = new SplitsWorkerImp(synchronizer); + ThreadFactory threadFactory, + SplitParser splitParser, + SplitCacheProducer splitCacheProducer) { + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, telemetryRuntimeProducer); Worker segmentWorker = new SegmentsWorkerImp(synchronizer); PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(statusMessages, telemetryRuntimeProducer); return new PushManagerImp(new AuthApiClientImp(authUrl, splitAPI.getHttpClient(), telemetryRuntimeProducer), - EventSourceClientImp.build(streamingUrl, splitsWorker, segmentWorker, pushStatusTracker, splitAPI.getSseHttpClient(), telemetryRuntimeProducer, threadFactory), - splitsWorker, + EventSourceClientImp.build(streamingUrl, featureFlagsWorker, segmentWorker, pushStatusTracker, splitAPI.getSseHttpClient(), telemetryRuntimeProducer, threadFactory), + featureFlagsWorker, segmentWorker, pushStatusTracker, telemetryRuntimeProducer, @@ -134,13 +138,13 @@ private boolean startSse(String token, String channels) { @Override public synchronized void startWorkers() { - _splitsWorker.start(); + _featureFlagsWorker.start(); _segmentWorker.start(); } @Override public synchronized void stopWorkers() { - _splitsWorker.stop(); + _featureFlagsWorker.stop(); _segmentWorker.stop(); } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/common/SyncManagerImp.java b/client/src/main/java/io/split/engine/common/SyncManagerImp.java index d739d40ab..bc23c96b1 100644 --- a/client/src/main/java/io/split/engine/common/SyncManagerImp.java +++ b/client/src/main/java/io/split/engine/common/SyncManagerImp.java @@ -5,6 +5,7 @@ import io.split.client.SplitClientConfig; import io.split.engine.SDKReadinessGates; import io.split.engine.experiments.SplitFetcher; +import io.split.engine.experiments.SplitParser; import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.segments.SegmentSynchronizationTask; import io.split.storages.SegmentCacheProducer; @@ -85,7 +86,8 @@ public static SyncManagerImp build(SplitTasks splitTasks, SDKReadinessGates gates, TelemetryRuntimeProducer telemetryRuntimeProducer, TelemetrySynchronizer telemetrySynchronizer, - SplitClientConfig config) { + SplitClientConfig config, + SplitParser splitParser) { LinkedBlockingQueue pushMessages = new LinkedBlockingQueue<>(); Synchronizer synchronizer = new SynchronizerImp(splitTasks, splitFetcher, @@ -102,7 +104,9 @@ public static SyncManagerImp build(SplitTasks splitTasks, splitAPI, pushMessages, telemetryRuntimeProducer, - config.getThreadFactory()); + config.getThreadFactory(), + splitParser, + splitCacheProducer); return new SyncManagerImp(splitTasks, config.streamingEnabled(), diff --git a/client/src/main/java/io/split/engine/common/Synchronizer.java b/client/src/main/java/io/split/engine/common/Synchronizer.java index 20dcae138..8885e8b16 100644 --- a/client/src/main/java/io/split/engine/common/Synchronizer.java +++ b/client/src/main/java/io/split/engine/common/Synchronizer.java @@ -1,12 +1,15 @@ package io.split.engine.common; +import io.split.engine.sse.dtos.SplitKillNotification; + public interface Synchronizer { boolean syncAll(); void startPeriodicFetching(); void stopPeriodicFetching(); void refreshSplits(Long targetChangeNumber); - void localKillSplit(String featureFlagName, String defaultTreatment, long newChangeNumber); + void localKillSplit(SplitKillNotification splitKillNotification); void refreshSegment(String segmentName, Long targetChangeNumber); void startPeriodicDataRecording(); void stopPeriodicDataRecording(); + void forceRefreshSegment(String segmentName); } diff --git a/client/src/main/java/io/split/engine/common/SynchronizerImp.java b/client/src/main/java/io/split/engine/common/SynchronizerImp.java index 71cad55f1..1e308dccd 100644 --- a/client/src/main/java/io/split/engine/common/SynchronizerImp.java +++ b/client/src/main/java/io/split/engine/common/SynchronizerImp.java @@ -9,6 +9,7 @@ import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.segments.SegmentFetcher; import io.split.engine.segments.SegmentSynchronizationTask; +import io.split.engine.sse.dtos.SplitKillNotification; import io.split.storages.SegmentCacheProducer; import io.split.storages.SplitCacheProducer; import io.split.telemetry.synchronizer.TelemetrySyncTask; @@ -183,10 +184,10 @@ public void refreshSplits(Long targetChangeNumber) { } @Override - public void localKillSplit(String featureFlagName, String defaultTreatment, long newChangeNumber) { - if (newChangeNumber > _splitCacheProducer.getChangeNumber()) { - _splitCacheProducer.kill(featureFlagName, defaultTreatment, newChangeNumber); - refreshSplits(newChangeNumber); + public void localKillSplit(SplitKillNotification splitKillNotification) { + if (splitKillNotification.getChangeNumber() > _splitCacheProducer.getChangeNumber()) { + _splitCacheProducer.kill(splitKillNotification.getSplitName(), splitKillNotification.getDefaultTreatment(), splitKillNotification.getChangeNumber()); + refreshSplits(splitKillNotification.getChangeNumber()); } } @@ -303,8 +304,9 @@ public void stopPeriodicDataRecording() { _log.info("Successful shutdown of telemetry sync task"); } - private void forceRefreshSegment(String segmentName){ + @Override + public void forceRefreshSegment(String segmentName){ SegmentFetcher segmentFetcher = _segmentSynchronizationTaskImp.getFetcher(segmentName); segmentFetcher.fetch(new FetchOptions.Builder().build()); } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java b/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java index 0afef5cc1..5eb81bda8 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java +++ b/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java @@ -1,9 +1,7 @@ package io.split.engine.experiments; -import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; -import io.split.client.dtos.Status; -import io.split.storages.SplitCacheConsumer; +import io.split.client.utils.FeatureFlagsToUpdate; import io.split.storages.SplitCacheProducer; import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; import io.split.telemetry.storage.TelemetryRuntimeProducer; @@ -11,12 +9,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.ArrayList; import java.util.HashSet; -import java.util.List; import java.util.Set; import static com.google.common.base.Preconditions.checkNotNull; +import static io.split.client.utils.FeatureFlagProcessor.processFeatureFlagChanges; /** * An ExperimentFetcher that refreshes experiment definitions periodically. @@ -29,7 +26,6 @@ public class SplitFetcherImp implements SplitFetcher { private final SplitParser _parser; private final SplitChangeFetcher _splitChangeFetcher; - private final SplitCacheConsumer _splitCacheConsumer; private final SplitCacheProducer _splitCacheProducer; private final Object _lock = new Object(); private final TelemetryRuntimeProducer _telemetryRuntimeProducer; @@ -44,10 +40,9 @@ public class SplitFetcherImp implements SplitFetcher { * an ARCHIVED split is received, we know if we need to remove a traffic type from the multiset. */ - public SplitFetcherImp(SplitChangeFetcher splitChangeFetcher, SplitParser parser, SplitCacheConsumer splitCacheConsumer, SplitCacheProducer splitCacheProducer, TelemetryRuntimeProducer telemetryRuntimeProducer) { + public SplitFetcherImp(SplitChangeFetcher splitChangeFetcher, SplitParser parser, SplitCacheProducer splitCacheProducer, TelemetryRuntimeProducer telemetryRuntimeProducer) { _splitChangeFetcher = checkNotNull(splitChangeFetcher); _parser = checkNotNull(parser); - _splitCacheConsumer = checkNotNull(splitCacheConsumer); _splitCacheProducer = checkNotNull(splitCacheProducer); _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } @@ -119,48 +114,11 @@ private Set runWithoutExceptionHandling(FetchOptions options) throws Int return segments; } - List parsedSplits = new ArrayList<>(); - for (Split split : change.splits) { - if (Thread.currentThread().isInterrupted()) { - throw new InterruptedException(); - } - - if (split.status != Status.ACTIVE) { - // archive. - _splitCacheProducer.remove(split.name); - continue; - } - - ParsedSplit parsedSplit = _parser.parse(split); - if (parsedSplit == null) { - _log.info(String.format("We could not parse the experiment definition for: %s so we are removing it completely to be careful", split.name)); - - _splitCacheProducer.remove(split.name); - _log.debug("Deleted feature: " + split.name); - - continue; - } - segments.addAll(parsedSplit.getSegmentsNames()); - - // If the split already exists, this is either an update, or the split has been - // deleted and recreated (possibly with a different traffic type). - // If it's an update, the traffic type should NOT be increased. - // If it's deleted & recreated, the old one should be decreased and the new one increased. - // To handle both cases, we simply delete the old one if the split is present. - // The new one is always increased. - ParsedSplit current = _splitCacheConsumer.get(split.name); // TODO (lecheverz): implement UPDATE method at Split Cache - if (current != null) { - _splitCacheProducer.remove(split.name); - } - - parsedSplits.add(parsedSplit); - _log.debug("Updated feature: " + parsedSplit.feature()); - } - - _splitCacheProducer.putMany(parsedSplits); - _splitCacheProducer.setChangeNumber(change.till); + FeatureFlagsToUpdate featureFlagsToUpdate = processFeatureFlagChanges(_parser, change.splits); + segments = featureFlagsToUpdate.getSegments(); + _splitCacheProducer.update(featureFlagsToUpdate.getToAdd(), featureFlagsToUpdate.getToRemove(), change.till); _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SPLITS, System.currentTimeMillis()); } return segments; } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/experiments/SplitParser.java b/client/src/main/java/io/split/engine/experiments/SplitParser.java index 7b521175e..eab521212 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitParser.java +++ b/client/src/main/java/io/split/engine/experiments/SplitParser.java @@ -6,7 +6,6 @@ import io.split.client.dtos.MatcherGroup; import io.split.client.dtos.Partition; import io.split.client.dtos.Split; -import io.split.client.dtos.Status; import io.split.engine.matchers.AllKeysMatcher; import io.split.engine.matchers.AttributeMatcher; import io.split.engine.matchers.BetweenMatcher; @@ -56,10 +55,6 @@ public ParsedSplit parse(Split split) { } private ParsedSplit parseWithoutExceptionHandling(Split split) { - if (split.status != Status.ACTIVE) { - return null; - } - List parsedConditionList = Lists.newArrayList(); for (Condition condition : split.conditions) { @@ -176,6 +171,4 @@ private AttributeMatcher toMatcher(Matcher matcher) { return new AttributeMatcher(attribute, delegate, negate); } - - -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/segments/SegmentFetcher.java b/client/src/main/java/io/split/engine/segments/SegmentFetcher.java index 0c8820b6e..3d5c3a48e 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentFetcher.java +++ b/client/src/main/java/io/split/engine/segments/SegmentFetcher.java @@ -9,7 +9,7 @@ public interface SegmentFetcher { /** * fetch */ - void fetch(FetchOptions opts); + boolean fetch(FetchOptions opts); boolean runWhitCacheHeader(); } diff --git a/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java b/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java index 1f9d5f91e..466a529a7 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java +++ b/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java @@ -1,6 +1,5 @@ package io.split.engine.segments; -import com.google.common.annotations.VisibleForTesting; import io.split.client.dtos.SegmentChange; import io.split.storages.SegmentCacheProducer; import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; @@ -34,14 +33,27 @@ public SegmentFetcherImp(String segmentName, SegmentChangeFetcher segmentChangeF } @Override - public void fetch(FetchOptions opts){ + public boolean fetch(FetchOptions opts){ try { - fetchUntil(opts); + final long INITIAL_CN = _segmentCacheProducer.getChangeNumber(_segmentName); + while (true) { + long start = _segmentCacheProducer.getChangeNumber(_segmentName); + runWithoutExceptionHandling(opts); + if (INITIAL_CN == start) { + opts = new FetchOptions.Builder(opts).targetChangeNumber(FetchOptions.DEFAULT_TARGET_CHANGENUMBER).build(); + } + long end = _segmentCacheProducer.getChangeNumber(_segmentName); + if (start >= end) { + break; + } + } + return true; } catch (Exception e){ _log.error("RefreshableSegmentFetcher failed: " + e.getMessage()); if (_log.isDebugEnabled()) { _log.debug("Reason:", e); } + return false; } } @@ -108,44 +120,8 @@ private String summarize(List changes) { return bldr.toString(); } - @VisibleForTesting - void fetchUntil(FetchOptions opts){ - final long INITIAL_CN = _segmentCacheProducer.getChangeNumber(_segmentName); - while (true) { - long start = _segmentCacheProducer.getChangeNumber(_segmentName); - runWithoutExceptionHandling(opts); - if (INITIAL_CN == start) { - opts = new FetchOptions.Builder(opts).targetChangeNumber(FetchOptions.DEFAULT_TARGET_CHANGENUMBER).build(); - } - long end = _segmentCacheProducer.getChangeNumber(_segmentName); - if (start >= end) { - break; - } - } - } - @Override public boolean runWhitCacheHeader(){ - return this.fetchAndUpdate(new FetchOptions.Builder().cacheControlHeaders(true).build()); - } - - /** - * Calls callLoopRun and after fetchs segment. - * @param opts contains all soft of options used when issuing the fetch request - */ - @VisibleForTesting - boolean fetchAndUpdate(FetchOptions opts) { - try { - // Do this again in case the previous call errored out. - fetchUntil(opts); - return true; - - } catch (Exception e){ - _log.error("RefreshableSegmentFetcher failed: " + e.getMessage()); - if (_log.isDebugEnabled()) { - _log.debug("Reason:", e); - } - return false; - } + return this.fetch(new FetchOptions.Builder().cacheControlHeaders(true).build()); } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/EventSourceClientImp.java b/client/src/main/java/io/split/engine/sse/EventSourceClientImp.java index 3791334bb..35d1c05d7 100644 --- a/client/src/main/java/io/split/engine/sse/EventSourceClientImp.java +++ b/client/src/main/java/io/split/engine/sse/EventSourceClientImp.java @@ -6,7 +6,7 @@ import io.split.engine.sse.client.SSEClient; import io.split.engine.sse.dtos.SegmentQueueDto; import io.split.engine.sse.exceptions.EventParsingException; -import io.split.engine.sse.workers.SplitsWorker; +import io.split.engine.sse.workers.FeatureFlagsWorker; import io.split.engine.sse.workers.Worker; import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -56,7 +56,7 @@ public class EventSourceClientImp implements EventSourceClient { } public static EventSourceClientImp build(String baseStreamingUrl, - SplitsWorker splitsWorker, + FeatureFlagsWorker featureFlagsWorker, Worker segmentWorker, PushStatusTracker pushStatusTracker, CloseableHttpClient sseHttpClient, @@ -64,7 +64,7 @@ public static EventSourceClientImp build(String baseStreamingUrl, ThreadFactory threadFactory) { return new EventSourceClientImp(baseStreamingUrl, new NotificationParserImp(), - NotificationProcessorImp.build(splitsWorker, segmentWorker, pushStatusTracker), + NotificationProcessorImp.build(featureFlagsWorker, segmentWorker, pushStatusTracker), pushStatusTracker, sseHttpClient, telemetryRuntimeProducer, diff --git a/client/src/main/java/io/split/engine/sse/NotificationParserImp.java b/client/src/main/java/io/split/engine/sse/NotificationParserImp.java index 802d94b54..e5fee7502 100644 --- a/client/src/main/java/io/split/engine/sse/NotificationParserImp.java +++ b/client/src/main/java/io/split/engine/sse/NotificationParserImp.java @@ -1,7 +1,16 @@ package io.split.engine.sse; import io.split.client.utils.Json; -import io.split.engine.sse.dtos.*; + +import io.split.engine.sse.dtos.ControlNotification; +import io.split.engine.sse.dtos.ErrorNotification; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.engine.sse.dtos.IncomingNotification; +import io.split.engine.sse.dtos.OccupancyNotification; +import io.split.engine.sse.dtos.RawMessageNotification; +import io.split.engine.sse.dtos.SegmentChangeNotification; +import io.split.engine.sse.dtos.SplitKillNotification; import io.split.engine.sse.exceptions.EventParsingException; public class NotificationParserImp implements NotificationParser { @@ -13,11 +22,9 @@ public IncomingNotification parseMessage(String payload) throws EventParsingExce RawMessageNotification rawMessageNotification = Json.fromJson(payload, RawMessageNotification.class); GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); genericNotificationData.setChannel(rawMessageNotification.getChannel()); - if (rawMessageNotification.getChannel().contains(OCCUPANCY_PREFIX)) { return parseControlChannelMessage(genericNotificationData); } - return parseNotification(genericNotificationData); } catch (Exception ex) { throw new EventParsingException("Error parsing event.", ex, payload); @@ -28,11 +35,9 @@ public IncomingNotification parseMessage(String payload) throws EventParsingExce public ErrorNotification parseError(String payload) throws EventParsingException { try { ErrorNotification messageError = Json.fromJson(payload, ErrorNotification.class); - if (messageError.getMessage() == null || messageError.getStatusCode() == null) { throw new Exception("Wrong notification format."); } - return messageError; } catch (Exception ex) { throw new EventParsingException("Error parsing event.", ex, payload); @@ -42,7 +47,7 @@ public ErrorNotification parseError(String payload) throws EventParsingException private IncomingNotification parseNotification(GenericNotificationData genericNotificationData) throws Exception { switch (genericNotificationData.getType()) { case SPLIT_UPDATE: - return new SplitChangeNotification(genericNotificationData); + return new FeatureFlagChangeNotification(genericNotificationData); case SPLIT_KILL: return new SplitKillNotification(genericNotificationData); case SEGMENT_UPDATE: @@ -59,7 +64,6 @@ private IncomingNotification parseControlChannelMessage(GenericNotificationData if (genericNotificationData.getControlType() != null) { return new ControlNotification(genericNotificationData); } - return new OccupancyNotification(genericNotificationData); } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/NotificationProcessor.java b/client/src/main/java/io/split/engine/sse/NotificationProcessor.java index 20f8af7fa..bdd842455 100644 --- a/client/src/main/java/io/split/engine/sse/NotificationProcessor.java +++ b/client/src/main/java/io/split/engine/sse/NotificationProcessor.java @@ -1,12 +1,14 @@ package io.split.engine.sse; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; import io.split.engine.sse.dtos.IncomingNotification; +import io.split.engine.sse.dtos.SplitKillNotification; import io.split.engine.sse.dtos.StatusNotification; public interface NotificationProcessor { void process(IncomingNotification notification); - void processSplitUpdate(long changeNumber); - void processSplitKill(long changeNumber, String splitName, String defaultTreatment); + void processSplitUpdate(FeatureFlagChangeNotification featureFlagChangeNotification); + void processSplitKill(SplitKillNotification splitKillNotification); void processSegmentUpdate(long changeNumber, String segmentName); void processStatus(StatusNotification statusNotification); -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java b/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java index c8271c9ec..5b8e705b3 100644 --- a/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java +++ b/client/src/main/java/io/split/engine/sse/NotificationProcessorImp.java @@ -1,30 +1,33 @@ package io.split.engine.sse; import com.google.common.annotations.VisibleForTesting; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.GenericNotificationData; import io.split.engine.sse.dtos.IncomingNotification; +import io.split.engine.sse.dtos.SplitKillNotification; import io.split.engine.sse.dtos.StatusNotification; import io.split.engine.sse.dtos.SegmentQueueDto; -import io.split.engine.sse.workers.SplitsWorker; +import io.split.engine.sse.workers.FeatureFlagsWorker; import io.split.engine.sse.workers.Worker; import static com.google.common.base.Preconditions.checkNotNull; public class NotificationProcessorImp implements NotificationProcessor { - private final SplitsWorker _splitsWorker; + private final FeatureFlagsWorker _featureFlagsWorker; private final Worker _segmentWorker; private final PushStatusTracker _pushStatusTracker; @VisibleForTesting - /* package private */ NotificationProcessorImp(SplitsWorker splitsWorker, + /* package private */ NotificationProcessorImp(FeatureFlagsWorker featureFlagsWorker, Worker segmentWorker, PushStatusTracker pushStatusTracker) { - _splitsWorker = checkNotNull(splitsWorker); + _featureFlagsWorker = checkNotNull(featureFlagsWorker); _segmentWorker = checkNotNull(segmentWorker); _pushStatusTracker = checkNotNull(pushStatusTracker); } - public static NotificationProcessorImp build(SplitsWorker splitsWorker, Worker segmentWorker, PushStatusTracker pushStatusTracker) { - return new NotificationProcessorImp(splitsWorker, segmentWorker, pushStatusTracker); + public static NotificationProcessorImp build(FeatureFlagsWorker featureFlagsWorker, Worker segmentWorker, PushStatusTracker pushStatusTracker) { + return new NotificationProcessorImp(featureFlagsWorker, segmentWorker, pushStatusTracker); } @Override @@ -33,14 +36,17 @@ public void process(IncomingNotification notification) { } @Override - public void processSplitUpdate(long changeNumber) { - _splitsWorker.addToQueue(changeNumber); + public void processSplitUpdate(FeatureFlagChangeNotification featureFlagChangeNotification) { + _featureFlagsWorker.addToQueue(featureFlagChangeNotification); } @Override - public void processSplitKill(long changeNumber, String splitName, String defaultTreatment) { - _splitsWorker.killSplit(changeNumber, splitName, defaultTreatment); - _splitsWorker.addToQueue(changeNumber); + public void processSplitKill(SplitKillNotification splitKillNotification) { + _featureFlagsWorker.kill(splitKillNotification); + _featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(splitKillNotification.getChangeNumber()) + .channel(splitKillNotification.getChannel()) + .build())); } @Override diff --git a/client/src/main/java/io/split/engine/sse/dtos/FeatureFlagChangeNotification.java b/client/src/main/java/io/split/engine/sse/dtos/FeatureFlagChangeNotification.java new file mode 100644 index 000000000..05f79abec --- /dev/null +++ b/client/src/main/java/io/split/engine/sse/dtos/FeatureFlagChangeNotification.java @@ -0,0 +1,80 @@ +package io.split.engine.sse.dtos; + +import io.split.client.dtos.Split; +import io.split.client.utils.Json; +import io.split.engine.segments.SegmentSynchronizationTaskImp; +import io.split.engine.sse.NotificationProcessor; +import io.split.engine.sse.enums.CompressType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Base64; +import java.util.zip.DataFormatException; + +import static io.split.engine.sse.utils.DecompressionUtil.gZipDecompress; +import static io.split.engine.sse.utils.DecompressionUtil.zLibDecompress; + +public class FeatureFlagChangeNotification extends IncomingNotification { + private static final Logger _log = LoggerFactory.getLogger(SegmentSynchronizationTaskImp.class); + private final long changeNumber; + private long previousChangeNumber; + private Split featureFlagDefinition; + private CompressType compressType; + + public FeatureFlagChangeNotification(GenericNotificationData genericNotificationData) { + super(Type.SPLIT_UPDATE, genericNotificationData.getChannel()); + changeNumber = genericNotificationData.getChangeNumber(); + if(genericNotificationData.getPreviousChangeNumber() != null) { + previousChangeNumber = genericNotificationData.getPreviousChangeNumber(); + } + compressType = CompressType.from(genericNotificationData.getCompressType()); + if (compressType == null || genericNotificationData.getFeatureFlagDefinition() == null) { + return; + } + try { + byte[] decodedBytes = Base64.getDecoder().decode(genericNotificationData.getFeatureFlagDefinition()); + switch (compressType) { + case GZIP: + decodedBytes = gZipDecompress(decodedBytes); + break; + case ZLIB: + decodedBytes = zLibDecompress(decodedBytes); + break; + } + featureFlagDefinition = Json.fromJson(new String(decodedBytes, 0, decodedBytes.length, "UTF-8"), Split.class); + } catch (UnsupportedEncodingException | IllegalArgumentException e) { + _log.warn("Could not decode base64 data in flag definition", e); + } catch (DataFormatException d) { + _log.warn("Could not decompress feature flag definition with zlib algorithm", d); + } catch (IOException i) { + _log.warn("Could not decompress feature flag definition with gzip algorithm", i); + } + } + + public long getChangeNumber() { + return changeNumber; + } + public long getPreviousChangeNumber() { + return previousChangeNumber; + } + + public Split getFeatureFlagDefinition() { + return featureFlagDefinition; + } + + public CompressType getCompressType() { + return compressType; + } + + @Override + public void handler(NotificationProcessor notificationProcessor) { + notificationProcessor.processSplitUpdate(this); + } + + @Override + public String toString() { + return String.format("Type: %s; Channel: %s; ChangeNumber: %s", getType(), getChannel(), getChangeNumber()); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java b/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java index 416ab8375..7fd3dc1bd 100644 --- a/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java +++ b/client/src/main/java/io/split/engine/sse/dtos/GenericNotificationData.java @@ -1,5 +1,7 @@ package io.split.engine.sse.dtos; +import com.google.gson.annotations.SerializedName; + public class GenericNotificationData { private final Long changeNumber; private final String defaultTreatment; @@ -9,15 +11,24 @@ public class GenericNotificationData { private final String segmentName; private final IncomingNotification.Type type; private String channel; + @SerializedName("pcn") + private Long previousChangeNumber; + @SerializedName("d") + private String featureFlagDefinition; + @SerializedName("c") + private Integer compressType; - public GenericNotificationData (Long changeNumber, + private GenericNotificationData (Long changeNumber, String defaultTreatment, String splitName, ControlType controlType, OccupancyMetrics occupancyMetrics, String segmentName, IncomingNotification.Type type, - String channel) { + String channel, + Long previousChangeNumber, + String data, + Integer compressType) { this.changeNumber = changeNumber; this.defaultTreatment = defaultTreatment; this.splitName = splitName; @@ -26,6 +37,9 @@ public GenericNotificationData (Long changeNumber, this.segmentName = segmentName; this.type = type; this.channel = channel; + this.previousChangeNumber = previousChangeNumber; + this.featureFlagDefinition = data; + this.compressType = compressType; } public long getChangeNumber() { @@ -57,8 +71,100 @@ public IncomingNotification.Type getType() { } public String getChannel() { return channel; } + public Long getPreviousChangeNumber() { + return previousChangeNumber; + } + + public String getFeatureFlagDefinition() { + return featureFlagDefinition; + } + + public Integer getCompressType() { + return compressType; + } public void setChannel(String channel) { this.channel = channel; } -} + + public static GenericNotificationData.Builder builder() { + return new GenericNotificationData.Builder(); + } + + public static final class Builder { + private Long changeNumber; + private String defaultTreatment; + private String featureFlagName; + private ControlType controlType; + private OccupancyMetrics metrics; + private String segmentName; + private IncomingNotification.Type type; + private String channel; + private Long previousChangeNumber; + private String featureFlagDefinition; + private Integer compressType; + + public Builder() { + } + + public Builder changeNumber(Long changeNumber) { + this.changeNumber = changeNumber; + return this; + } + + public Builder defaultTreatment(String defaultTreatment) { + this.defaultTreatment = defaultTreatment; + return this; + } + + public Builder featureFlagName(String featureFlagName) { + this.featureFlagName = featureFlagName; + return this; + } + + public Builder controlType(ControlType controlType) { + this.controlType = controlType; + return this; + } + + public Builder metrics(OccupancyMetrics occupancyMetrics) { + this.metrics = occupancyMetrics; + return this; + } + + public Builder segmentName(String segmentName) { + this.segmentName = segmentName; + return this; + } + + public Builder type(IncomingNotification.Type type) { + this.type = type; + return this; + } + + public Builder channel(String channel) { + this.channel = channel; + return this; + } + + public Builder previousChangeNumber(Long previousChangeNumber) { + this.previousChangeNumber = previousChangeNumber; + return this; + } + + public Builder featureFlagDefinition(String featureFlagDefinition) { + this.featureFlagDefinition = featureFlagDefinition; + return this; + } + + public Builder compressType(Integer compressType) { + this.compressType = compressType; + return this; + } + + public GenericNotificationData build() { + return new GenericNotificationData(changeNumber, defaultTreatment, featureFlagName, controlType, metrics, + segmentName, type, channel, previousChangeNumber, featureFlagDefinition, compressType); + } + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/dtos/RawAuthResponse.java b/client/src/main/java/io/split/engine/sse/dtos/RawAuthResponse.java index 4e0026420..4082aac72 100644 --- a/client/src/main/java/io/split/engine/sse/dtos/RawAuthResponse.java +++ b/client/src/main/java/io/split/engine/sse/dtos/RawAuthResponse.java @@ -60,4 +60,4 @@ private String addPrefixControlChannels(String channels) { .replace("control_pri", "[?occupancy=metrics.publishers]control_pri") .replace("control_sec", "[?occupancy=metrics.publishers]control_sec"); } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/dtos/SplitChangeNotification.java b/client/src/main/java/io/split/engine/sse/dtos/SplitChangeNotification.java deleted file mode 100644 index 56b8c32c5..000000000 --- a/client/src/main/java/io/split/engine/sse/dtos/SplitChangeNotification.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.split.engine.sse.dtos; - -import io.split.engine.sse.NotificationProcessor; - -public class SplitChangeNotification extends IncomingNotification { - private final long changeNumber; - - public SplitChangeNotification(GenericNotificationData genericNotificationData) { - super(Type.SPLIT_UPDATE, genericNotificationData.getChannel()); - this.changeNumber = genericNotificationData.getChangeNumber(); - } - - public long getChangeNumber() { - return changeNumber; - } - - @Override - public void handler(NotificationProcessor notificationProcessor) { - notificationProcessor.processSplitUpdate(getChangeNumber()); - } - - @Override - public String toString() { - return String.format("Type: %s; Channel: %s; ChangeNumber: %s", getType(), getChannel(), getChangeNumber()); - } -} diff --git a/client/src/main/java/io/split/engine/sse/dtos/SplitKillNotification.java b/client/src/main/java/io/split/engine/sse/dtos/SplitKillNotification.java index ed4700352..4d47e758b 100644 --- a/client/src/main/java/io/split/engine/sse/dtos/SplitKillNotification.java +++ b/client/src/main/java/io/split/engine/sse/dtos/SplitKillNotification.java @@ -28,11 +28,11 @@ public String getSplitName() { @Override public void handler(NotificationProcessor notificationProcessor) { - notificationProcessor.processSplitKill(getChangeNumber(), getSplitName(), getDefaultTreatment()); + notificationProcessor.processSplitKill(this); } @Override public String toString() { return String.format("Type: %s; Channel: %s; ChangeNumber: %s; DefaultTreatment: %s; SplitName: %s", getType(), getChannel(), getChangeNumber(), getDefaultTreatment(), getSplitName()); } -} +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/enums/CompressType.java b/client/src/main/java/io/split/engine/sse/enums/CompressType.java new file mode 100644 index 000000000..ed78a33b8 --- /dev/null +++ b/client/src/main/java/io/split/engine/sse/enums/CompressType.java @@ -0,0 +1,39 @@ +package io.split.engine.sse.enums; + +import java.util.HashMap; +import java.util.Map; + +public enum CompressType { + NOT_COMPRESSED(0), + GZIP(1), + ZLIB(2); + + private final Integer value; + + CompressType(Integer value) { + this.value = value; + } + + public long getValue() { + return value; + } + + // Mapping compress type to compress type id + private static final Map _map = new HashMap<>(); + static { + for (CompressType compressType : CompressType.values()) + _map.put(compressType.value, compressType); + } + + /** + * Get compress type from value + * @param value value + * @return CompressType + */ + public static CompressType from(Integer value) { + if (value == null || _map.size() <= value){ + return null; + } + return _map.get(value); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/utils/DecompressionUtil.java b/client/src/main/java/io/split/engine/sse/utils/DecompressionUtil.java new file mode 100644 index 000000000..522425e45 --- /dev/null +++ b/client/src/main/java/io/split/engine/sse/utils/DecompressionUtil.java @@ -0,0 +1,42 @@ +package io.split.engine.sse.utils; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.zip.DataFormatException; +import java.util.zip.GZIPInputStream; +import java.util.zip.Inflater; + +public class DecompressionUtil { + + public static byte[] zLibDecompress(byte[] toDecompress) throws DataFormatException { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(toDecompress.length); + Inflater decompressor = new Inflater(); + try { + decompressor.setInput(toDecompress); + final byte[] buf = new byte[toDecompress.length]; + while (!decompressor.finished()) { + int count = decompressor.inflate(buf); + byteArrayOutputStream.write(buf, 0, count); + } + } finally { + decompressor.end(); + } + return byteArrayOutputStream.toByteArray(); + } + + public static byte[] gZipDecompress(byte[] toDecompress) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + try (GZIPInputStream gzipInputStream = new GZIPInputStream(new ByteArrayInputStream(toDecompress))){ + int res = 0; + byte buf[] = new byte[toDecompress.length]; + while (res >= 0) { + res = gzipInputStream.read(buf, 0, buf.length); + if (res > 0) { + out.write(buf, 0, res); + } + } + } + return out.toByteArray(); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/workers/FeatureFlagWorkerImp.java b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagWorkerImp.java new file mode 100644 index 000000000..455986689 --- /dev/null +++ b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagWorkerImp.java @@ -0,0 +1,77 @@ +package io.split.engine.sse.workers; + +import io.split.client.dtos.Split; +import io.split.client.utils.FeatureFlagsToUpdate; +import io.split.engine.common.Synchronizer; +import io.split.engine.experiments.SplitParser; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.SplitKillNotification; +import io.split.storages.SplitCacheProducer; +import io.split.telemetry.domain.enums.UpdatesFromSSEEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collections; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.split.client.utils.FeatureFlagProcessor.processFeatureFlagChanges; + +public class FeatureFlagWorkerImp extends Worker implements FeatureFlagsWorker { + private static final Logger _log = LoggerFactory.getLogger(FeatureFlagWorkerImp.class); + private final Synchronizer _synchronizer; + private final SplitParser _splitParser; + private final SplitCacheProducer _splitCacheProducer; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + + public FeatureFlagWorkerImp(Synchronizer synchronizer, SplitParser splitParser, SplitCacheProducer splitCacheProducer, TelemetryRuntimeProducer telemetryRuntimeProducer) { + super("Feature flags"); + _synchronizer = checkNotNull(synchronizer); + _splitParser = splitParser; + _splitCacheProducer = splitCacheProducer; + _telemetryRuntimeProducer = telemetryRuntimeProducer; + } + + @Override + public void kill(SplitKillNotification splitKillNotification) { + try { + _synchronizer.localKillSplit(splitKillNotification); + _log.debug(String.format("Kill feature flag: %s, changeNumber: %s, defaultTreatment: %s", splitKillNotification.getSplitName(), splitKillNotification.getChangeNumber(), + splitKillNotification.getDefaultTreatment())); + } catch (Exception ex) { + _log.warn(String.format("Exception on FeatureFlagWorker kill: %s", ex.getMessage())); + } + } + + @Override + protected void executeRefresh(FeatureFlagChangeNotification featureFlagChangeNotification) { + boolean success = addOrUpdateFeatureFlag(featureFlagChangeNotification); + + if (!success) + _synchronizer.refreshSplits(featureFlagChangeNotification.getChangeNumber()); + } + + private boolean addOrUpdateFeatureFlag(FeatureFlagChangeNotification featureFlagChangeNotification) { + if (featureFlagChangeNotification.getChangeNumber() <= _splitCacheProducer.getChangeNumber()) { + return true; + } + try { + if (featureFlagChangeNotification.getFeatureFlagDefinition() != null && + featureFlagChangeNotification.getPreviousChangeNumber() == _splitCacheProducer.getChangeNumber()) { + Split featureFlag = featureFlagChangeNotification.getFeatureFlagDefinition(); + FeatureFlagsToUpdate featureFlagsToUpdate = processFeatureFlagChanges(_splitParser, Collections.singletonList(featureFlag)); + _splitCacheProducer.update(featureFlagsToUpdate.getToAdd(), featureFlagsToUpdate.getToRemove(), featureFlagChangeNotification.getChangeNumber()); + Set segments = featureFlagsToUpdate.getSegments(); + for (String segmentName: segments) { + _synchronizer.forceRefreshSegment(segmentName); + } + _telemetryRuntimeProducer.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + return true; + } + } catch (Exception e) { + _log.warn("Something went wrong processing a Feature Flag notification", e); + } + return false; + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/workers/FeatureFlagsWorker.java b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagsWorker.java new file mode 100644 index 000000000..354dbd7e1 --- /dev/null +++ b/client/src/main/java/io/split/engine/sse/workers/FeatureFlagsWorker.java @@ -0,0 +1,11 @@ +package io.split.engine.sse.workers; + +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.SplitKillNotification; + +public interface FeatureFlagsWorker { + void addToQueue(FeatureFlagChangeNotification featureFlagChangeNotification); + void start(); + void stop(); + void kill(SplitKillNotification splitKillNotification); +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/workers/SplitsWorker.java b/client/src/main/java/io/split/engine/sse/workers/SplitsWorker.java deleted file mode 100644 index 3664b7cd4..000000000 --- a/client/src/main/java/io/split/engine/sse/workers/SplitsWorker.java +++ /dev/null @@ -1,8 +0,0 @@ -package io.split.engine.sse.workers; - -public interface SplitsWorker { - void addToQueue(Long element); - void start(); - void stop(); - void killSplit(long changeNumber, String splitName, String defaultTreatment); -} diff --git a/client/src/main/java/io/split/engine/sse/workers/SplitsWorkerImp.java b/client/src/main/java/io/split/engine/sse/workers/SplitsWorkerImp.java deleted file mode 100644 index 16e155ed7..000000000 --- a/client/src/main/java/io/split/engine/sse/workers/SplitsWorkerImp.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.split.engine.sse.workers; - -import io.split.engine.common.Synchronizer; - -import static com.google.common.base.Preconditions.checkNotNull; - -public class SplitsWorkerImp extends Worker implements SplitsWorker { - private final Synchronizer _synchronizer; - - public SplitsWorkerImp(Synchronizer synchronizer) { - super("Splits"); - _synchronizer = checkNotNull(synchronizer); - } - - @Override - public void killSplit(long changeNumber, String splitName, String defaultTreatment) { - try { - _synchronizer.localKillSplit(splitName, defaultTreatment, changeNumber); - _log.debug(String.format("Kill split: %s, changeNumber: %s, defaultTreatment: %s", splitName, changeNumber, defaultTreatment)); - } catch (Exception ex) { - _log.warn(String.format("Exception on SplitWorker killSplit: %s", ex.getMessage())); - } - } - - @Override - protected void executeRefresh(Long changeNumber) { - _synchronizer.refreshSplits(changeNumber); - } -} diff --git a/client/src/main/java/io/split/storages/SplitCacheProducer.java b/client/src/main/java/io/split/storages/SplitCacheProducer.java index a237b06f0..02a64ddc9 100644 --- a/client/src/main/java/io/split/storages/SplitCacheProducer.java +++ b/client/src/main/java/io/split/storages/SplitCacheProducer.java @@ -12,4 +12,5 @@ public interface SplitCacheProducer extends SplitCacheCommons{ void putMany(List splits); void increaseTrafficType(String trafficType); void decreaseTrafficType(String trafficType); + void update(List toAdd, List toRemove, long changeNumber); } diff --git a/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java b/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java index 4a4c97a67..167741730 100644 --- a/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java +++ b/client/src/main/java/io/split/storages/memory/InMemoryCacheImp.java @@ -142,7 +142,20 @@ public void increaseTrafficType(String trafficType) { public void decreaseTrafficType(String trafficType) { _concurrentTrafficTypeNameSet.remove(trafficType); } - + + @Override + public void update(List toAdd, List toRemove, long changeNumber) { + if(toAdd != null) { + putMany(toAdd); + } + if(toRemove != null) { + for(String featureFlag : toRemove) { + remove(featureFlag); + } + } + setChangeNumber(changeNumber); + } + public Set getSegments() { return _concurrentMap.values().stream() .flatMap(parsedSplit -> parsedSplit.getSegmentsNames().stream()).collect(Collectors.toSet()); diff --git a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterProducer.java b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterProducer.java index a98391443..b9a034aff 100644 --- a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterProducer.java +++ b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomSplitAdapterProducer.java @@ -98,6 +98,19 @@ public void decreaseTrafficType(String trafficType) { } } + @Override + public void update(List toAdd, List toRemove, long changeNumber) { + if(toAdd != null) { + putMany(toAdd); + } + if(toRemove != null) { + for(String featureFlag : toRemove) { + remove(featureFlag); + } + } + setChangeNumber(changeNumber); + } + @Override public Set getSegments() { //NoOp diff --git a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomTelemetryAdapterProducer.java b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomTelemetryAdapterProducer.java index 802b8402a..1dd9412b7 100644 --- a/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomTelemetryAdapterProducer.java +++ b/client/src/main/java/io/split/storages/pluggable/adapters/UserCustomTelemetryAdapterProducer.java @@ -10,6 +10,7 @@ import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; import io.split.telemetry.domain.enums.MethodEnum; import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.domain.enums.UpdatesFromSSEEnum; import io.split.telemetry.storage.TelemetryStorageProducer; import io.split.telemetry.utils.BucketCalculator; import pluggable.CustomStorageWrapper; @@ -99,4 +100,9 @@ public void recordStreamingEvents(StreamingEvent streamingEvent) { public void recordSessionLength(long sessionLength) { //No-op } + + @Override + public void recordUpdatesFromSSE(UpdatesFromSSEEnum updatesFromSSEEnum) { + //No-op + } } \ No newline at end of file diff --git a/client/src/main/java/io/split/telemetry/domain/Stats.java b/client/src/main/java/io/split/telemetry/domain/Stats.java index 8baec7261..31577caf5 100644 --- a/client/src/main/java/io/split/telemetry/domain/Stats.java +++ b/client/src/main/java/io/split/telemetry/domain/Stats.java @@ -23,6 +23,7 @@ public class Stats { /* package private */ static final String FIELD_EVENTS_DROPPED = "eD"; /* package private */ static final String FIELD_STREAMING_EVENT = "sE"; /* package private */ static final String FIELD_TAGS = "t"; + /* package private */ static final String FIELD_UPDATES_FROM_SSE = "ufs"; @SerializedName(FIELD_LAST_SYNCHRONIZATION) private LastSynchronization _lastSynchronization; @@ -60,6 +61,8 @@ public class Stats { private List _streamingEvents; @SerializedName(FIELD_TAGS) private List _tags; + @SerializedName(FIELD_UPDATES_FROM_SSE) + private UpdatesFromSSE _updatesFromSSE; public LastSynchronization get_lastSynchronization() { return _lastSynchronization; @@ -204,4 +207,12 @@ public List get_tags() { public void set_tags(List _tags) { this._tags = _tags; } + + public UpdatesFromSSE get_updatesFromSSE() { + return _updatesFromSSE; + } + + public void set_updatesFromSSE(UpdatesFromSSE _updatesFromSSE) { + this._updatesFromSSE = _updatesFromSSE; + } } diff --git a/client/src/main/java/io/split/telemetry/domain/UpdatesFromSSE.java b/client/src/main/java/io/split/telemetry/domain/UpdatesFromSSE.java new file mode 100644 index 000000000..1f2238f9c --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/UpdatesFromSSE.java @@ -0,0 +1,19 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +public class UpdatesFromSSE { + + /* package private */ static final String FIELD_FEATURE_FLAGS = "sp"; + + @SerializedName(FIELD_FEATURE_FLAGS) + private long splits; + + public long getSplits() { + return splits; + } + + public void setSplits(long splits) { + this.splits = splits; + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/telemetry/domain/enums/UpdatesFromSSEEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/UpdatesFromSSEEnum.java new file mode 100644 index 000000000..eceb872e6 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/UpdatesFromSSEEnum.java @@ -0,0 +1,5 @@ +package io.split.telemetry.domain.enums; + +public enum UpdatesFromSSEEnum { + SPLITS +} \ No newline at end of file diff --git a/client/src/main/java/io/split/telemetry/storage/InMemoryTelemetryStorage.java b/client/src/main/java/io/split/telemetry/storage/InMemoryTelemetryStorage.java index 493a34b75..0d950d8f6 100644 --- a/client/src/main/java/io/split/telemetry/storage/InMemoryTelemetryStorage.java +++ b/client/src/main/java/io/split/telemetry/storage/InMemoryTelemetryStorage.java @@ -1,8 +1,24 @@ package io.split.telemetry.storage; import com.google.common.collect.Maps; -import io.split.telemetry.domain.*; -import io.split.telemetry.domain.enums.*; + +import io.split.telemetry.domain.HTTPErrors; +import io.split.telemetry.domain.HTTPLatencies; +import io.split.telemetry.domain.LastSynchronization; +import io.split.telemetry.domain.MethodExceptions; +import io.split.telemetry.domain.MethodLatencies; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.UpdatesFromSSE; +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.domain.enums.FactoryCountersEnum; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.MethodEnum; +import io.split.telemetry.domain.enums.PushCountersEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.domain.enums.SdkRecordsEnum; +import io.split.telemetry.domain.enums.UpdatesFromSSEEnum; import io.split.telemetry.utils.AtomicLongArray; import io.split.telemetry.utils.BucketCalculator; @@ -13,7 +29,7 @@ import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; -public class InMemoryTelemetryStorage implements TelemetryStorage{ +public class InMemoryTelemetryStorage implements TelemetryStorage{ public static final int MAX_LATENCY_BUCKET_COUNT = 23; public static final int MAX_STREAMING_EVENTS = 20; public static final int MAX_TAGS = 10; @@ -32,6 +48,7 @@ public class InMemoryTelemetryStorage implements TelemetryStorage{ private final ConcurrentMap _eventsDataRecords = Maps.newConcurrentMap(); private final ConcurrentMap _lastSynchronizationRecords = Maps.newConcurrentMap(); private final ConcurrentMap _sdkRecords = Maps.newConcurrentMap(); + private final ConcurrentMap _updatesFromSSERecords = Maps.newConcurrentMap(); //HTTPErrors private final ConcurrentMap> _httpErrors = Maps.newConcurrentMap(); @@ -55,6 +72,7 @@ public InMemoryTelemetryStorage() { initSdkRecords(); initLastSynchronizationRecords(); initEventDataRecords(); + initUpdatesFromSEE(); } @Override @@ -209,6 +227,13 @@ public long getSessionLength() { return _sdkRecords.get(SdkRecordsEnum.SESSION).get(); } + @Override + public UpdatesFromSSE popUpdatesFromSSE() { + UpdatesFromSSE updatesFromSSE = new UpdatesFromSSE(); + updatesFromSSE.setSplits(_updatesFromSSERecords.get(UpdatesFromSSEEnum.SPLITS).getAndSet(0L)); + return updatesFromSSE; + } + @Override public void addTag(String tag) { synchronized (_tagsLock) { @@ -271,6 +296,11 @@ public void recordSessionLength(long sessionLength) { _sdkRecords.replace(SdkRecordsEnum.SESSION, new AtomicLong(sessionLength)); } + @Override + public void recordUpdatesFromSSE(UpdatesFromSSEEnum updatesFromSSEEnum) { + _updatesFromSSERecords.get(UpdatesFromSSEEnum.SPLITS).incrementAndGet(); + } + private void initMethodLatencies() { _methodLatencies.put(MethodEnum.TREATMENT, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); _methodLatencies.put(MethodEnum.TREATMENTS, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); @@ -341,4 +371,8 @@ private void initEventDataRecords() { _eventsDataRecords.put(EventsDataRecordsEnum.EVENTS_DROPPED, new AtomicLong()); _eventsDataRecords.put(EventsDataRecordsEnum.EVENTS_QUEUED, new AtomicLong()); } -} + + private void initUpdatesFromSEE() { + _updatesFromSSERecords.put(UpdatesFromSSEEnum.SPLITS, new AtomicLong()); + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/telemetry/storage/NoopTelemetryStorage.java b/client/src/main/java/io/split/telemetry/storage/NoopTelemetryStorage.java index 3673d0c0a..d18134a8a 100644 --- a/client/src/main/java/io/split/telemetry/storage/NoopTelemetryStorage.java +++ b/client/src/main/java/io/split/telemetry/storage/NoopTelemetryStorage.java @@ -1,7 +1,19 @@ package io.split.telemetry.storage; -import io.split.telemetry.domain.*; -import io.split.telemetry.domain.enums.*; +import io.split.telemetry.domain.HTTPErrors; +import io.split.telemetry.domain.HTTPLatencies; +import io.split.telemetry.domain.LastSynchronization; +import io.split.telemetry.domain.MethodExceptions; +import io.split.telemetry.domain.MethodLatencies; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.UpdatesFromSSE; +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.MethodEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.domain.enums.UpdatesFromSSEEnum; import java.util.List; @@ -77,6 +89,11 @@ public void recordSessionLength(long sessionLength) { } + @Override + public void recordUpdatesFromSSE(UpdatesFromSSEEnum updatesFromSSEEnum) { + + } + @Override public long getBURTimeouts() { return 0; @@ -146,4 +163,9 @@ public List popTags() { public long getSessionLength() { return 0; } + + @Override + public UpdatesFromSSE popUpdatesFromSSE() { + return null; + } } diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeConsumer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeConsumer.java index 6a746e783..8be689989 100644 --- a/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeConsumer.java +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeConsumer.java @@ -4,6 +4,7 @@ import io.split.telemetry.domain.HTTPLatencies; import io.split.telemetry.domain.LastSynchronization; import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.UpdatesFromSSE; import io.split.telemetry.domain.enums.EventsDataRecordsEnum; import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; @@ -20,4 +21,5 @@ public interface TelemetryRuntimeConsumer { List popStreamingEvents(); List popTags(); long getSessionLength(); + UpdatesFromSSE popUpdatesFromSSE(); } diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeProducer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeProducer.java index 2baf016f0..789562871 100644 --- a/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeProducer.java +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeProducer.java @@ -1,6 +1,7 @@ package io.split.telemetry.storage; import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.UpdatesFromSSE; import io.split.telemetry.domain.enums.*; public interface TelemetryRuntimeProducer { @@ -14,4 +15,5 @@ public interface TelemetryRuntimeProducer { void recordTokenRefreshes(); void recordStreamingEvents(StreamingEvent streamingEvent); void recordSessionLength(long sessionLength); -} + void recordUpdatesFromSSE(UpdatesFromSSEEnum updatesFromSSEEnum); +} \ No newline at end of file diff --git a/client/src/main/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitter.java b/client/src/main/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitter.java index 5666e1258..c58ed1380 100644 --- a/client/src/main/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitter.java +++ b/client/src/main/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitter.java @@ -92,6 +92,7 @@ Stats generateStats() throws Exception { stats.set_eventsDropped(_teleTelemetryStorageConsumer.getEventStats(EventsDataRecordsEnum.EVENTS_DROPPED)); stats.set_streamingEvents(_teleTelemetryStorageConsumer.popStreamingEvents()); stats.set_tags(_teleTelemetryStorageConsumer.popTags()); + stats.set_updatesFromSSE(_teleTelemetryStorageConsumer.popUpdatesFromSSE()); return stats; } diff --git a/client/src/test/java/io/split/client/utils/FeatureFlagProcessorTest.java b/client/src/test/java/io/split/client/utils/FeatureFlagProcessorTest.java new file mode 100644 index 000000000..5c81d12bf --- /dev/null +++ b/client/src/test/java/io/split/client/utils/FeatureFlagProcessorTest.java @@ -0,0 +1,34 @@ +package io.split.client.utils; + +import io.split.client.dtos.Split; +import io.split.engine.experiments.SplitParser; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static io.split.client.utils.FeatureFlagProcessor.processFeatureFlagChanges; + +public class FeatureFlagProcessorTest { + + @Test + public void testProcessFeatureFlagChanges() { + SplitParser splitParser = new SplitParser(); + List featureFlags = new ArrayList<>(); + + String definition1 = "{\"trafficTypeName\":\"user\",\"id\":\"d431cdd0-b0be-11ea-8a80-1660ada9ce39\",\"name\":\"mauro_java\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-92391491,\"seed\":-1769377604,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1684329854385,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"WHITELIST\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"matcherType\":\"WHITELIST\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"admin\",\"mauro\",\"nico\"]}}]},\"partitions\":[{\"treatment\":\"off\",\"size\":100}],\"label\":\"whitelisted\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"maur-2\"}}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100},{\"treatment\":\"V4\",\"size\":0},{\"treatment\":\"v5\",\"size\":0}],\"label\":\"in segment maur-2\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100},{\"treatment\":\"V4\",\"size\":0},{\"treatment\":\"v5\",\"size\":0}],\"label\":\"default rule\"}]}"; + Split featureFlagTest1 = Json.fromJson(definition1, Split.class); + + String definition2 = "{\"trafficTypeName\":\"user\",\"id\":\"d704f220-0567-11ee-80ee-fa3c6460cd13\",\"name\":\"NET_CORE_getTreatmentWithConfigAfterArchive\",\"trafficAllocation\":100,\"trafficAllocationSeed\":179018541,\"seed\":272707374,\"status\":\"ARCHIVED\",\"killed\":false,\"defaultTreatment\":\"V-FGyN\",\"changeNumber\":1686165617166,\"algo\":2,\"configurations\":{\"V-FGyN\":\"{\\\"color\\\":\\\"blue\\\"}\",\"V-YrWB\":\"{\\\"color\\\":\\\"red\\\"}\"},\"conditions\":[{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\",\"attribute\":\"test\"},\"matcherType\":\"LESS_THAN_OR_EQUAL_TO\",\"negate\":false,\"unaryNumericMatcherData\":{\"dataType\":\"NUMBER\",\"value\":20}}]},\"partitions\":[{\"treatment\":\"V-FGyN\",\"size\":0},{\"treatment\":\"V-YrWB\",\"size\":100}],\"label\":\"test \\u003c\\u003d 20\"}]}"; + Split featureFlagTest2 = Json.fromJson(definition2, Split.class); + + featureFlags.add(featureFlagTest1); + featureFlags.add(featureFlagTest2); + FeatureFlagsToUpdate featureFlagsToUpdate = processFeatureFlagChanges(splitParser, featureFlags); + + Assert.assertEquals(1, featureFlagsToUpdate.toAdd.size()); + Assert.assertEquals(1, featureFlagsToUpdate.toRemove.size()); + Assert.assertEquals(1, featureFlagsToUpdate.segments.size()); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java b/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java index a7fbdeacf..3d303845e 100644 --- a/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java +++ b/client/src/test/java/io/split/engine/common/LocalhostSynchronizerTest.java @@ -11,7 +11,6 @@ import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.storages.SegmentCacheProducer; import io.split.storages.SplitCache; -import io.split.storages.SplitCacheConsumer; import io.split.storages.SplitCacheProducer; import io.split.storages.memory.InMemoryCacheImp; import io.split.storages.memory.SegmentCacheInMemoryImpl; @@ -26,14 +25,13 @@ public class LocalhostSynchronizerTest { private static final TelemetryStorage TELEMETRY_STORAGE_NOOP = Mockito.mock(NoopTelemetryStorage.class); @Test - public void testSyncAll(){ + public void testSyncAll() { SplitCache splitCacheProducer = new InMemoryCacheImp(); - SplitCacheConsumer splitCacheConsumer = Mockito.mock(SplitCacheConsumer.class); SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher("src/test/resources/split_init.json"); SplitParser splitParser = new SplitParser(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheConsumer, splitCacheProducer, TELEMETRY_STORAGE_NOOP); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000L, null); SegmentChangeFetcher segmentChangeFetcher = new LocalhostSegmentChangeFetcher("src/test/resources/"); @@ -51,12 +49,11 @@ public void testSyncAll(){ @Test public void testPeriodicFetching() throws InterruptedException { SplitCache splitCacheProducer = new InMemoryCacheImp(); - SplitCacheConsumer splitCacheConsumer = Mockito.mock(SplitCacheConsumer.class); SplitChangeFetcher splitChangeFetcher = Mockito.mock(JsonLocalhostSplitChangeFetcher.class); SplitParser splitParser = new SplitParser(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheConsumer, splitCacheProducer, TELEMETRY_STORAGE_NOOP); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000L, null); FetchOptions fetchOptions = new FetchOptions.Builder().build(); @@ -77,13 +74,12 @@ public void testPeriodicFetching() throws InterruptedException { } @Test - public void testRefreshSplits(){ + public void testRefreshSplits() { SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(); - SplitCacheConsumer splitCacheConsumer = Mockito.mock(SplitCacheConsumer.class); SplitChangeFetcher splitChangeFetcher = Mockito.mock(SplitChangeFetcher.class); SplitParser splitParser = new SplitParser(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheConsumer, splitCacheProducer, TELEMETRY_STORAGE_NOOP); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000L, null); SplitTasks splitTasks = SplitTasks.build(splitSynchronizationTask, null, null, null, null, null); LocalhostSynchronizer localhostSynchronizer = new LocalhostSynchronizer(splitTasks, splitFetcher, false); diff --git a/client/src/test/java/io/split/engine/common/PushManagerTest.java b/client/src/test/java/io/split/engine/common/PushManagerTest.java index d95d13ce5..12664ad1d 100644 --- a/client/src/test/java/io/split/engine/common/PushManagerTest.java +++ b/client/src/test/java/io/split/engine/common/PushManagerTest.java @@ -7,7 +7,7 @@ import io.split.engine.sse.client.SSEClient; import io.split.engine.sse.dtos.AuthenticationResponse; import io.split.engine.sse.workers.SegmentsWorkerImp; -import io.split.engine.sse.workers.SplitsWorker; +import io.split.engine.sse.workers.FeatureFlagsWorker; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.TelemetryStorage; import org.junit.Assert; @@ -32,7 +32,7 @@ public void setUp() { _telemetryStorage = new InMemoryTelemetryStorage(); _pushManager = new PushManagerImp(_authApiClient, _eventSourceClient, - Mockito.mock(SplitsWorker.class), + Mockito.mock(FeatureFlagsWorker.class), Mockito.mock(SegmentsWorkerImp.class), _pushStatusTracker, _telemetryStorage, diff --git a/client/src/test/java/io/split/engine/common/SynchronizerTest.java b/client/src/test/java/io/split/engine/common/SynchronizerTest.java index a25ec4139..036115a93 100644 --- a/client/src/test/java/io/split/engine/common/SynchronizerTest.java +++ b/client/src/test/java/io/split/engine/common/SynchronizerTest.java @@ -5,7 +5,11 @@ import io.split.client.impressions.UniqueKeysTracker; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTaskImp; -import io.split.storages.*; +import io.split.storages.SegmentCache; +import io.split.storages.SegmentCacheProducer; +import io.split.storages.SplitCache; +import io.split.storages.SplitCacheConsumer; +import io.split.storages.SplitCacheProducer; import io.split.storages.memory.InMemoryCacheImp; import io.split.engine.experiments.FetchResult; import io.split.engine.experiments.SplitFetcherImp; @@ -113,7 +117,7 @@ public void stopPeriodicFetching() { public void streamingRetryOnSplit() { when(_splitCacheProducer.getChangeNumber()).thenReturn(0l).thenReturn(0l).thenReturn(1l); when(_splitFetcher.forceRefresh(Mockito.anyObject())).thenReturn(new FetchResult(true, new HashSet<>())); - _synchronizer.refreshSplits(1l); + _synchronizer.refreshSplits(1L); Mockito.verify(_splitCacheProducer, Mockito.times(3)).getChangeNumber(); } @@ -138,14 +142,14 @@ public void streamingRetryOnSplitAndSegment() { SegmentFetcher fetcher = Mockito.mock(SegmentFetcher.class); when(_segmentCacheProducer.getChangeNumber(Mockito.anyString())).thenReturn(0l).thenReturn(0l).thenReturn(1l); when(_segmentFetcher.getFetcher(Mockito.anyString())).thenReturn(fetcher); - _synchronizer.refreshSplits(1l); + _synchronizer.refreshSplits(1L); Mockito.verify(_splitCacheProducer, Mockito.times(3)).getChangeNumber(); Mockito.verify(_segmentFetcher, Mockito.times(2)).getFetcher(Mockito.anyString()); } @Test - public void testCDNBypassIsRequestedAfterNFailures() throws NoSuchFieldException, IllegalAccessException { + public void testCDNBypassIsRequestedAfterNFailures() { SplitCache cache = new InMemoryCacheImp(); Synchronizer imp = new SynchronizerImp(_splitTasks, @@ -317,4 +321,4 @@ public void testDataRecording(){ Mockito.verify(_uniqueKeysTracker, Mockito.times(1)).stop(); Mockito.verify(_telemetrySyncTask, Mockito.times(1)).stopScheduledTask(); } -} +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java b/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java index b1de841a0..d838cc0de 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitFetcherImpTest.java @@ -2,7 +2,6 @@ import io.split.client.JsonLocalhostSplitChangeFetcher; import io.split.engine.common.FetchOptions; -import io.split.storages.SplitCacheConsumer; import io.split.storages.SplitCacheProducer; import io.split.storages.memory.InMemoryCacheImp; import io.split.telemetry.storage.NoopTelemetryStorage; @@ -18,12 +17,11 @@ public class SplitFetcherImpTest { @Test public void testLocalHost(){ SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(); - SplitCacheConsumer splitCacheConsumer = Mockito.mock(SplitCacheConsumer.class); SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher("src/test/resources/split_init.json"); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheConsumer, splitCacheProducer, TELEMETRY_STORAGE_NOOP); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP); FetchResult fetchResult = splitFetcher.forceRefresh(fetchOptions); diff --git a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java index f27168587..a6be86240 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java @@ -36,6 +36,7 @@ import static org.hamcrest.Matchers.not; import static org.hamcrest.Matchers.greaterThan; import static org.hamcrest.Matchers.nullValue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; import static org.mockito.Matchers.*; import static org.mockito.Mockito.mock; @@ -63,13 +64,12 @@ public void works_when_we_start_with_any_state() throws InterruptedException { private void works(long startingChangeNumber) throws InterruptedException { AChangePerCallSplitChangeFetcher splitChangeFetcher = new AChangePerCallSplitChangeFetcher(); SplitCache cache = new InMemoryCacheImp(startingChangeNumber); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, cache, TELEMETRY_STORAGE); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 3, TimeUnit.SECONDS); assertThat(splitChangeFetcher.lastAdded(), is(greaterThan(startingChangeNumber))); -// assertThat(cache.getChangeNumber(), is(equalTo(splitChangeFetcher.lastAdded()))); // all previous splits have been removed since they are dead for (long i = startingChangeNumber; i < cache.getChangeNumber(); i++) { @@ -135,14 +135,14 @@ public void when_parser_fails_we_remove_the_experiment() throws InterruptedExcep SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null); segmentSynchronizationTask.start(); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, cache, TELEMETRY_STORAGE); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); - assertThat(cache.getChangeNumber(), is(equalTo(1L))); + assertEquals(1L, cache.getChangeNumber()); // verify that the fetcher return null - assertThat(cache.get("-1"), is(nullValue())); + Assert.assertNull(cache.get("-1")); } @Test @@ -156,12 +156,12 @@ public void if_there_is_a_problem_talking_to_split_change_count_down_latch_is_no SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, TELEMETRY_STORAGE, cache, null); segmentSynchronizationTask.start(); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(),cache, cache, TELEMETRY_STORAGE); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); - assertThat(cache.getChangeNumber(), is(equalTo(-1L))); + Assert.assertEquals(-1L, cache.getChangeNumber()); } private void executeWaitAndTerminate(Runnable runnable, long frequency, long waitInBetween, TimeUnit unit) throws InterruptedException { @@ -197,7 +197,7 @@ public void works_with_user_defined_segments() throws Exception { when(segmentChangeFetcher.fetch(anyString(), anyLong(), any())).thenReturn(segmentChange); SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, segmentCache, Mockito.mock(TelemetryStorage.class), cache, null); segmentSynchronizationTask.start(); - SplitFetcherImp fetcher = new SplitFetcherImp(experimentChangeFetcher, new SplitParser(), cache, cache, TELEMETRY_STORAGE); + SplitFetcherImp fetcher = new SplitFetcherImp(experimentChangeFetcher, new SplitParser(), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -217,7 +217,7 @@ public void testBypassCdnClearedAfterFirstHit() { SplitChangeFetcher mockFetcher = Mockito.mock(SplitChangeFetcher.class); SplitParser mockParser = new SplitParser(); SplitCache mockCache = new InMemoryCacheImp(); - SplitFetcherImp fetcher = new SplitFetcherImp(mockFetcher, mockParser, mockCache, mockCache, Mockito.mock(TelemetryRuntimeProducer.class)); + SplitFetcherImp fetcher = new SplitFetcherImp(mockFetcher, mockParser, mockCache, Mockito.mock(TelemetryRuntimeProducer.class)); SplitChange response1 = new SplitChange(); diff --git a/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java b/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java index 1d3417795..d53ac89e7 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitSynchronizationTaskTest.java @@ -2,7 +2,6 @@ import io.split.client.JsonLocalhostSplitChangeFetcher; import io.split.engine.common.FetchOptions; -import io.split.storages.SplitCacheConsumer; import io.split.storages.SplitCacheProducer; import io.split.storages.memory.InMemoryCacheImp; import io.split.telemetry.storage.NoopTelemetryStorage; @@ -17,12 +16,11 @@ public class SplitSynchronizationTaskTest { @Test public void testLocalhost() throws InterruptedException { SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(); - SplitCacheConsumer splitCacheConsumer = Mockito.mock(SplitCacheConsumer.class); SplitChangeFetcher splitChangeFetcher = Mockito.mock(JsonLocalhostSplitChangeFetcher.class); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheConsumer, splitCacheProducer, TELEMETRY_STORAGE_NOOP); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000, null); diff --git a/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java b/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java index d9803d6cf..ce263bfe4 100644 --- a/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java +++ b/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java @@ -1,6 +1,5 @@ package io.split.engine.segments; -import com.google.common.collect.Sets; import io.split.storages.SegmentCache; import io.split.storages.SegmentCacheProducer; import io.split.storages.memory.SegmentCacheInMemoryImpl; @@ -18,7 +17,6 @@ import java.util.ArrayList; import java.util.List; -import java.util.Set; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -45,7 +43,6 @@ public void works_when_we_start_without_state() throws InterruptedException { @Test public void works_when_we_start_with_state() throws InterruptedException { works(20L); - } @Test @@ -76,11 +73,8 @@ public void works_when_there_are_no_changes() throws InterruptedException { Thread.currentThread().interrupt(); } - Set expected = Sets.newHashSet("" + (startingChangeNumber + 1)); - assertNotNull(segmentCache.getChangeNumber(SEGMENT_NAME)); assertEquals(10L, segmentCache.getChangeNumber(SEGMENT_NAME)); - } private void works(long startingChangeNumber) throws InterruptedException { @@ -114,7 +108,6 @@ private void works(long startingChangeNumber) throws InterruptedException { Thread.currentThread().interrupt(); } Mockito.verify(segmentChangeFetcher, Mockito.times(2)).fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyObject()); - } @@ -150,7 +143,7 @@ public void testBypassCdnClearedAfterFirstHit() { response2.added = new ArrayList<>(); response2.removed = new ArrayList<>(); response2.since = 1; - response1.till = 1; + response2.till = 1; ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class); ArgumentCaptor cnCaptor = ArgumentCaptor.forClass(Long.class); diff --git a/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java b/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java index d4a71ccbd..f9e1e354d 100644 --- a/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java +++ b/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java @@ -33,8 +33,7 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertEquals; /** * Tests for SegmentSynchronizationTaskImp @@ -93,8 +92,8 @@ public void run() { Thread.currentThread().interrupt(); } - assertThat(fetcher1.get(), is(notNullValue())); - assertThat(fetcher1.get(), is(sameInstance(fetcher2.get()))); + Assert.assertNotNull(fetcher1.get()); + assertEquals(fetcher1.get(), fetcher2.get()); } @Test @@ -107,10 +106,8 @@ public void testFetchAllAsynchronousAndGetFalse() throws NoSuchFieldException, I _segmentFetchers.put("SF", segmentFetcher); final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, segmentCacheProducer, TELEMETRY_STORAGE, Mockito.mock(SplitCacheConsumer.class), null); - Mockito.doNothing().when(segmentFetcher).fetchUntil(Mockito.anyObject()); Mockito.when(segmentFetcher.runWhitCacheHeader()).thenReturn(false); - Mockito.when(segmentFetcher.fetchAndUpdate(Mockito.anyObject())).thenReturn(false); - Mockito.doNothing().when(segmentFetcher).fetchUntil(Mockito.anyObject()); + Mockito.when(segmentFetcher.fetch(Mockito.anyObject())).thenReturn(false); // Before executing, we'll update the map of segmentFecthers via reflection. Field segmentFetchersForced = SegmentSynchronizationTaskImp.class.getDeclaredField("_segmentFetchers"); @@ -141,9 +138,8 @@ public void testFetchAllAsynchronousAndGetTrue() throws NoSuchFieldException, Il modifiersField.setAccessible(true); modifiersField.setInt(segmentFetchersForced, segmentFetchersForced.getModifiers() & ~Modifier.FINAL); segmentFetchersForced.set(fetchers, _segmentFetchers); - Mockito.doNothing().when(segmentFetcher).fetchUntil(Mockito.anyObject()); Mockito.when(segmentFetcher.runWhitCacheHeader()).thenReturn(true); - Mockito.when(segmentFetcher.fetchAndUpdate(Mockito.anyObject())).thenReturn(true); + Mockito.when(segmentFetcher.fetch(Mockito.anyObject())).thenReturn(true); boolean fetch = fetchers.fetchAllSynchronous(); Assert.assertEquals(true, fetch); } @@ -152,12 +148,11 @@ public void testFetchAllAsynchronousAndGetTrue() throws NoSuchFieldException, Il public void testLocalhostSegmentChangeFetcher() throws InterruptedException { SplitCache splitCacheProducer = new InMemoryCacheImp(); - SplitCache splitCacheConsumer = new InMemoryCacheImp(); SplitChangeFetcher splitChangeFetcher = new JsonLocalhostSplitChangeFetcher("src/test/resources/split_init.json"); SplitParser splitParser = new SplitParser(); FetchOptions fetchOptions = new FetchOptions.Builder().build(); - SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheConsumer, splitCacheProducer, TELEMETRY_STORAGE_NOOP); + SplitFetcher splitFetcher = new SplitFetcherImp(splitChangeFetcher, splitParser, splitCacheProducer, TELEMETRY_STORAGE_NOOP); SplitSynchronizationTask splitSynchronizationTask = new SplitSynchronizationTask(splitFetcher, splitCacheProducer, 1000, null); diff --git a/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java b/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java index d3bbc22c8..c5bc22b1b 100644 --- a/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java +++ b/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java @@ -3,7 +3,7 @@ import io.split.SSEMockServer; import io.split.engine.sse.client.SSEClient; import io.split.engine.sse.dtos.ErrorNotification; -import io.split.engine.sse.dtos.SplitChangeNotification; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; import io.split.telemetry.storage.InMemoryTelemetryStorage; import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.config.RequestConfig; @@ -44,7 +44,7 @@ public void startShouldConnect() throws IOException { EventSourceClient eventSourceClient = new EventSourceClientImp("http://localhost:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient(), telemetryRuntimeProducer, null); - boolean result = eventSourceClient.start("channel-test","token-test"); + boolean result = eventSourceClient.start("channel-test", "token-test"); Assert.assertTrue(result); @@ -59,7 +59,7 @@ public void startShouldReconnect() throws IOException { sseServer.start(); EventSourceClient eventSourceClient = new EventSourceClientImp("http://fake:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient(), telemetryRuntimeProducer, null); - boolean result = eventSourceClient.start("channel-test","token-test"); + boolean result = eventSourceClient.start("channel-test", "token-test"); Assert.assertFalse(result); @@ -76,7 +76,7 @@ public void startAndReceiveNotification() throws IOException { sseServer.start(); EventSourceClient eventSourceClient = new EventSourceClientImp("http://localhost:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient(), telemetryRuntimeProducer, null); - boolean result = eventSourceClient.start("channel-test","token-test"); + boolean result = eventSourceClient.start("channel-test", "token-test"); Assert.assertTrue(result); @@ -95,7 +95,7 @@ public void startAndReceiveNotification() throws IOException { Awaitility.await() .atMost(50L, TimeUnit.SECONDS) - .untilAsserted(() -> Mockito.verify(_notificationProcessor, Mockito.times(1)).process(Mockito.any(SplitChangeNotification.class))); + .untilAsserted(() -> Mockito.verify(_notificationProcessor, Mockito.times(1)).process(Mockito.any(FeatureFlagChangeNotification.class))); OutboundSseEvent sseEventError = new OutboundEvent .Builder() diff --git a/client/src/test/java/io/split/engine/sse/NotificationParserImpTest.java b/client/src/test/java/io/split/engine/sse/NotificationParserImpTest.java new file mode 100644 index 000000000..cd57e56ac --- /dev/null +++ b/client/src/test/java/io/split/engine/sse/NotificationParserImpTest.java @@ -0,0 +1,67 @@ +package io.split.engine.sse; + +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.enums.CompressType; +import io.split.engine.sse.exceptions.EventParsingException; + +import org.junit.Assert; +import org.junit.Test; + +public class NotificationParserImpTest { + + @Test + public void validateZlibCompressType() throws EventParsingException { + String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":2,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; + NotificationParserImp notificationParserImp = new NotificationParserImp(); + + FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); + Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().name, "mauro_java"); + Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().changeNumber, 1684265694505L); + Assert.assertEquals(CompressType.ZLIB, incomingNotification.getCompressType()); + Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); + } + + @Test + public void validateGzipCompressType() throws EventParsingException { + String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":1,\\\"d\\\":\\\"H4sIAAAAAAAA/8yT327aTBDFXyU612vJxoTgvUMfKB8qcaSapqoihAZ7DNusvWi9TpUiv3tl/pdQVb1qL+cwc3bOj/EGzlKeq3T6tuaYCoZEXbGFgMogkXXDIM0y31v4C/aCgMnrU9/3gl7Pp4yilMMIAuVusqDamvlXeiWIg/FAa5OSU6aEDHz/ip4wZ5Be1AmjoBsFAtVOCO56UXh31/O7ApUjV1eQGPw3HT+NIPCitG7bctIVC2ScU63d1DK5gksHCZPnEEhXVC45rosFW8ig1++GYej3g85tJEB6aSA7Aqkpc7Ws7XahCnLTbLVM7evnzalsUUHi8//j6WgyTqYQKMilK7b31tRryLa3WKiyfRCDeHhq2Dntiys+JS/J8THUt5VyrFXlHnYTQ3LU2h91yGdQVqhy+0RtTeuhUoNZ08wagTVZdxbBndF5vYVApb7z9m9pZgKaFqwhT+6coRHvg398nEweP/157Bd+S1hz6oxtm88O73B0jbhgM47nyej+YRRfgdNODDlXJWcJL9tUF5SqnRqfbtPr4LdcTHnk4rfp3buLOkG7+Pmp++vRM9w/wVblzX7Pm8OGfxf5YDKZfxh9SS6B/2Pc9t/7ja01o5k1PwIAAP//uTipVskEAAA=\\\"}\"}"; + NotificationParserImp notificationParserImp = new NotificationParserImp(); + + FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); + Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().name, "mauro_java"); + Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().changeNumber, 1684333081259L); + Assert.assertEquals(CompressType.GZIP, incomingNotification.getCompressType()); + Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); + } + + @Test + public void validateNotCompressType() throws EventParsingException { + String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684329854385,\\\"pcn\\\":0,\\\"c\\\":0,\\\"d\\\":\\\"eyJ0cmFmZmljVHlwZU5hbWUiOiJ1c2VyIiwiaWQiOiJkNDMxY2RkMC1iMGJlLTExZWEtOGE4MC0xNjYwYWRhOWNlMzkiLCJuYW1lIjoibWF1cm9famF2YSIsInRyYWZmaWNBbGxvY2F0aW9uIjoxMDAsInRyYWZmaWNBbGxvY2F0aW9uU2VlZCI6LTkyMzkxNDkxLCJzZWVkIjotMTc2OTM3NzYwNCwic3RhdHVzIjoiQUNUSVZFIiwia2lsbGVkIjpmYWxzZSwiZGVmYXVsdFRyZWF0bWVudCI6Im9mZiIsImNoYW5nZU51bWJlciI6MTY4NDMyOTg1NDM4NSwiYWxnbyI6MiwiY29uZmlndXJhdGlvbnMiOnt9LCJjb25kaXRpb25zIjpbeyJjb25kaXRpb25UeXBlIjoiV0hJVEVMSVNUIiwibWF0Y2hlckdyb3VwIjp7ImNvbWJpbmVyIjoiQU5EIiwibWF0Y2hlcnMiOlt7Im1hdGNoZXJUeXBlIjoiV0hJVEVMSVNUIiwibmVnYXRlIjpmYWxzZSwid2hpdGVsaXN0TWF0Y2hlckRhdGEiOnsid2hpdGVsaXN0IjpbImFkbWluIiwibWF1cm8iLCJuaWNvIl19fV19LCJwYXJ0aXRpb25zIjpbeyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9XSwibGFiZWwiOiJ3aGl0ZWxpc3RlZCJ9LHsiY29uZGl0aW9uVHlwZSI6IlJPTExPVVQiLCJtYXRjaGVyR3JvdXAiOnsiY29tYmluZXIiOiJBTkQiLCJtYXRjaGVycyI6W3sia2V5U2VsZWN0b3IiOnsidHJhZmZpY1R5cGUiOiJ1c2VyIn0sIm1hdGNoZXJUeXBlIjoiSU5fU0VHTUVOVCIsIm5lZ2F0ZSI6ZmFsc2UsInVzZXJEZWZpbmVkU2VnbWVudE1hdGNoZXJEYXRhIjp7InNlZ21lbnROYW1lIjoibWF1ci0yIn19XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImluIHNlZ21lbnQgbWF1ci0yIn0seyJjb25kaXRpb25UeXBlIjoiUk9MTE9VVCIsIm1hdGNoZXJHcm91cCI6eyJjb21iaW5lciI6IkFORCIsIm1hdGNoZXJzIjpbeyJrZXlTZWxlY3RvciI6eyJ0cmFmZmljVHlwZSI6InVzZXIifSwibWF0Y2hlclR5cGUiOiJBTExfS0VZUyIsIm5lZ2F0ZSI6ZmFsc2V9XX0sInBhcnRpdGlvbnMiOlt7InRyZWF0bWVudCI6Im9uIiwic2l6ZSI6MH0seyJ0cmVhdG1lbnQiOiJvZmYiLCJzaXplIjoxMDB9LHsidHJlYXRtZW50IjoiVjQiLCJzaXplIjowfSx7InRyZWF0bWVudCI6InY1Iiwic2l6ZSI6MH1dLCJsYWJlbCI6ImRlZmF1bHQgcnVsZSJ9XX0=\\\"}\"}"; + NotificationParserImp notificationParserImp = new NotificationParserImp(); + + FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); + Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().name, "mauro_java"); + Assert.assertEquals(incomingNotification.getFeatureFlagDefinition().changeNumber, 1684329854385L); + Assert.assertEquals(CompressType.NOT_COMPRESSED, incomingNotification.getCompressType()); + Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); + } + + @Test + public void validateCompressTypeIncorrect() throws EventParsingException { + String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":3,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; + NotificationParserImp notificationParserImp = new NotificationParserImp(); + + FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); + Assert.assertNull(incomingNotification.getCompressType()); + Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); + } + + @Test + public void validateCompressTypeNull() throws EventParsingException { + String payload = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; + NotificationParserImp notificationParserImp = new NotificationParserImp(); + + FeatureFlagChangeNotification incomingNotification = (FeatureFlagChangeNotification) notificationParserImp.parseMessage(payload); + Assert.assertNull(incomingNotification.getCompressType()); + Assert.assertEquals(0, incomingNotification.getPreviousChangeNumber()); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/NotificationParserTest.java b/client/src/test/java/io/split/engine/sse/NotificationParserTest.java index 1bbf355ae..26f9e1997 100644 --- a/client/src/test/java/io/split/engine/sse/NotificationParserTest.java +++ b/client/src/test/java/io/split/engine/sse/NotificationParserTest.java @@ -1,11 +1,18 @@ package io.split.engine.sse; -import io.split.engine.sse.dtos.*; +import io.split.engine.sse.dtos.ControlNotification; +import io.split.engine.sse.dtos.ControlType; +import io.split.engine.sse.dtos.ErrorNotification; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.IncomingNotification; +import io.split.engine.sse.dtos.OccupancyNotification; +import io.split.engine.sse.dtos.SegmentChangeNotification; +import io.split.engine.sse.dtos.SplitKillNotification; import io.split.engine.sse.exceptions.EventParsingException; import org.junit.Before; import org.junit.Test; -import static org.junit.Assert.*; +import static org.junit.Assert.assertEquals; public class NotificationParserTest { private NotificationParser notificationParser; @@ -22,7 +29,7 @@ public void parseSplitUpdateShouldReturnParsedEvent() throws EventParsingExcepti IncomingNotification result = notificationParser.parseMessage(payload); assertEquals(IncomingNotification.Type.SPLIT_UPDATE, result.getType()); assertEquals("xxxx_xxxx_splits", result.getChannel()); - assertEquals(1592590435115L, ((SplitChangeNotification) result).getChangeNumber()); + assertEquals(1592590435115L, ((FeatureFlagChangeNotification) result).getChangeNumber()); } @Test @@ -149,4 +156,4 @@ public void parseControlStreamingDisabledShouldReturnParsedEvent() throws EventP assertEquals("control_pri", result.getChannel()); assertEquals(ControlType.STREAMING_DISABLED, ((ControlNotification)result).getControlType()); } -} +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java b/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java index 4bbbaab72..a56f05dd1 100644 --- a/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java +++ b/client/src/test/java/io/split/engine/sse/NotificationProcessorTest.java @@ -1,38 +1,47 @@ package io.split.engine.sse; -import io.split.engine.sse.dtos.*; +import io.split.engine.sse.dtos.ControlNotification; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.engine.sse.dtos.OccupancyNotification; +import io.split.engine.sse.dtos.SegmentChangeNotification; +import io.split.engine.sse.dtos.SegmentQueueDto; +import io.split.engine.sse.dtos.SplitKillNotification; import io.split.engine.sse.workers.SegmentsWorkerImp; -import io.split.engine.sse.workers.SplitsWorker; +import io.split.engine.sse.workers.FeatureFlagsWorker; import io.split.engine.sse.workers.Worker; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; public class NotificationProcessorTest { - private SplitsWorker _splitsWorker; + private FeatureFlagsWorker _featureFlagsWorker; private Worker _segmentWorker; private NotificationProcessor _notificationProcessor; private PushStatusTracker _pushStatusTracker; @Before public void setUp() { - _splitsWorker = Mockito.mock(SplitsWorker.class); + _featureFlagsWorker = Mockito.mock(FeatureFlagsWorker.class); _segmentWorker = Mockito.mock(SegmentsWorkerImp.class); _pushStatusTracker = Mockito.mock(PushStatusTracker.class); - _notificationProcessor = new NotificationProcessorImp(_splitsWorker, _segmentWorker, _pushStatusTracker); + _notificationProcessor = new NotificationProcessorImp(_featureFlagsWorker, _segmentWorker, _pushStatusTracker); } @Test public void processSplitUpdateAddToQueueInWorker() { long changeNumber = 1585867723838L; String channel = "splits"; - GenericNotificationData genericNotificationData = new GenericNotificationData(changeNumber, null, null, null, null, null, null, channel); - SplitChangeNotification splitChangeNotification = new SplitChangeNotification(genericNotificationData); + GenericNotificationData genericNotificationData = GenericNotificationData.builder() + .changeNumber(changeNumber) + .channel(channel) + .build(); + FeatureFlagChangeNotification splitChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); _notificationProcessor.process(splitChangeNotification); - Mockito.verify(_splitsWorker, Mockito.times(1)).addToQueue(splitChangeNotification.getChangeNumber()); + Mockito.verify(_featureFlagsWorker, Mockito.times(1)).addToQueue(Mockito.anyObject()); } @Test @@ -41,13 +50,18 @@ public void processSplitKillAndAddToQueueInWorker() { String defaultTreatment = "off"; String splitName = "test-split"; String channel = "splits"; - GenericNotificationData genericNotificationData = new GenericNotificationData(changeNumber, defaultTreatment, splitName, null, null, null, null, channel); + GenericNotificationData genericNotificationData = GenericNotificationData.builder() + .changeNumber(changeNumber) + .defaultTreatment(defaultTreatment) + .featureFlagName(splitName) + .channel(channel) + .build(); SplitKillNotification splitKillNotification = new SplitKillNotification(genericNotificationData); _notificationProcessor.process(splitKillNotification); - Mockito.verify(_splitsWorker, Mockito.times(1)).killSplit(splitKillNotification.getChangeNumber(), splitKillNotification.getSplitName(), splitKillNotification.getDefaultTreatment()); - Mockito.verify(_splitsWorker, Mockito.times(1)).addToQueue(splitKillNotification.getChangeNumber()); + Mockito.verify(_featureFlagsWorker, Mockito.times(1)).kill(splitKillNotification); + Mockito.verify(_featureFlagsWorker, Mockito.times(1)).addToQueue(Mockito.anyObject()); } @Test @@ -55,7 +69,11 @@ public void processSegmentUpdateAddToQueueInWorker() { long changeNumber = 1585867723838L; String segmentName = "segment-test"; String channel = "segments"; - GenericNotificationData genericNotificationData = new GenericNotificationData(changeNumber, null, null, null, null, segmentName, null, channel); + GenericNotificationData genericNotificationData = GenericNotificationData.builder() + .changeNumber(changeNumber) + .segmentName(segmentName) + .channel(channel) + .build(); SegmentChangeNotification segmentChangeNotification = new SegmentChangeNotification(genericNotificationData); _notificationProcessor.process(segmentChangeNotification); @@ -75,11 +93,13 @@ public void processControlNotification() { @Test public void processOccupancyNotification() { - GenericNotificationData genericNotificationData = new GenericNotificationData(null, null, null, null, null, null, null, "control_pri"); + GenericNotificationData genericNotificationData = GenericNotificationData.builder() + .channel("control_pri") + .build(); OccupancyNotification occupancyNotification = new OccupancyNotification(genericNotificationData); _notificationProcessor.process(occupancyNotification); Mockito.verify(_pushStatusTracker, Mockito.times(1)).handleIncomingOccupancyEvent(Mockito.any(OccupancyNotification.class)); } -} +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/PushStatusTrackerTest.java b/client/src/test/java/io/split/engine/sse/PushStatusTrackerTest.java index 8e245f5af..2660f9e1c 100644 --- a/client/src/test/java/io/split/engine/sse/PushStatusTrackerTest.java +++ b/client/src/test/java/io/split/engine/sse/PushStatusTrackerTest.java @@ -10,12 +10,6 @@ import org.mockito.Mockito; import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.atomic.AtomicReference; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; public class PushStatusTrackerTest { private static final String CONTROL_PRI = "control_pri"; @@ -28,8 +22,8 @@ public void HandleControlEventStreamingPausedShouldNotifyEvent() { PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingControlEvent(controlNotification); - assertThat(messages.size(), is(equalTo(1))); - assertThat(messages.peek(), is(equalTo(PushManager.Status.STREAMING_DOWN))); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(PushManager.Status.STREAMING_DOWN, messages.peek()); } @Test @@ -40,9 +34,9 @@ public void HandleControlEventStreamingResumedShouldNotifyEvent() throws Interru pushStatusTracker.handleIncomingControlEvent(buildControlNotification(ControlType.STREAMING_PAUSED)); pushStatusTracker.handleIncomingControlEvent(buildControlNotification(ControlType.STREAMING_RESUMED)); - assertThat(messages.size(), is(equalTo(2))); - assertThat(messages.take(), is(equalTo(PushManager.Status.STREAMING_DOWN))); - assertThat(messages.take(), is(equalTo(PushManager.Status.STREAMING_READY))); + Assert.assertEquals(2, messages.size()); + Assert.assertEquals(PushManager.Status.STREAMING_DOWN, messages.take()); + Assert.assertEquals(PushManager.Status.STREAMING_READY, messages.take()); } @Test @@ -57,8 +51,8 @@ public void HandleControlEventStreamingResumedShouldNotNotifyEvent() { pushStatusTracker.handleIncomingControlEvent(controlNotification); pushStatusTracker.handleIncomingControlEvent(controlNotification); - assertThat(messages.size(), is(equalTo(1))); - assertThat(messages.peek(), is(equalTo(PushManager.Status.STREAMING_DOWN))); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(PushManager.Status.STREAMING_DOWN, messages.peek()); Assert.assertEquals(1, telemetryStorage.popStreamingEvents().size()); } @@ -72,8 +66,8 @@ public void HandleControlEventStreamingDisabledShouldNotifyShutdownEvent() { pushStatusTracker.handleIncomingControlEvent(controlNotification); pushStatusTracker.handleIncomingControlEvent(controlNotification); - assertThat(messages.size(), is(equalTo(1))); - assertThat(messages.peek(), is(equalTo(PushManager.Status.STREAMING_OFF))); + Assert.assertEquals(1, messages.size()); + Assert.assertEquals(PushManager.Status.STREAMING_OFF, messages.peek()); } @Test @@ -84,7 +78,7 @@ public void HandleOccupancyEventWithPublishersFirstTimeShouldNotNotifyEvent() { PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingOccupancyEvent(occupancyNotification); - assertThat(messages.size(), is(equalTo(0))); + Assert.assertEquals(0, messages.size()); Assert.assertEquals(1, telemetryStorage.popStreamingEvents().size()); } @@ -96,12 +90,12 @@ public void HandleOccupancyEventWithPublishersAndWithStreamingDisabledShouldNoti pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(0, null)); pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(2, null)); - assertThat(messages.size(), is(equalTo(2))); + Assert.assertEquals(2, messages.size()); PushManager.Status m1 = messages.take(); - assertThat(m1, is(equalTo(PushManager.Status.STREAMING_DOWN))); + Assert.assertEquals(PushManager.Status.STREAMING_DOWN, m1); PushManager.Status m2 = messages.take(); - assertThat(m2, is(equalTo(PushManager.Status.STREAMING_READY))); + Assert.assertEquals(PushManager.Status.STREAMING_READY, m2); } @Test @@ -112,12 +106,12 @@ public void HandleOccupancyEventWithDifferentChannelsPublishersShouldNotifyEvent pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(0, "control_pri")); pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(2, "control_sec")); - assertThat(messages.size(), is(equalTo(2))); + Assert.assertEquals(2, messages.size()); PushManager.Status m1 = messages.take(); - assertThat(m1, is(equalTo(PushManager.Status.STREAMING_DOWN))); + Assert.assertEquals(PushManager.Status.STREAMING_DOWN, m1); PushManager.Status m2 = messages.take(); - assertThat(m2, is(equalTo(PushManager.Status.STREAMING_READY))); + Assert.assertEquals(PushManager.Status.STREAMING_READY, m2); } @Test @@ -151,13 +145,12 @@ public void HandleTwoRetryableErrorInARow() throws InterruptedException { pushStatusTracker.handleSseStatus(SSEClient.StatusMessage.RETRYABLE_ERROR); pushStatusTracker.handleSseStatus(SSEClient.StatusMessage.RETRYABLE_ERROR); - - assertThat(messages.size(), is(equalTo(2))); + Assert.assertEquals(2, messages.size()); PushManager.Status m1 = messages.take(); - assertThat(m1, is(equalTo(PushManager.Status.STREAMING_BACKOFF))); + Assert.assertEquals(PushManager.Status.STREAMING_BACKOFF, m1); PushManager.Status m2 = messages.take(); - assertThat(m2, is(equalTo(PushManager.Status.STREAMING_BACKOFF))); + Assert.assertEquals(PushManager.Status.STREAMING_BACKOFF, m2); } private ControlNotification buildControlNotification(ControlType controlType) { @@ -169,14 +162,11 @@ private OccupancyNotification buildOccupancyNotification(int publishers, String } private GenericNotificationData buildGenericData(ControlType controlType, IncomingNotification.Type type, Integer publishers, String channel) { - return new GenericNotificationData( - null, - null, - null, - controlType, - publishers != null ? new OccupancyMetrics(publishers) : null, - null, - type, - channel == null ? "channel-test" : channel); + return GenericNotificationData.builder() + .controlType(controlType) + .metrics(publishers != null ? new OccupancyMetrics(publishers) : null) + .type(type) + .channel(channel == null ? "channel-test" : channel) + .build(); } -} +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/utils/DecompressionUtilTest.java b/client/src/test/java/io/split/engine/sse/utils/DecompressionUtilTest.java new file mode 100644 index 000000000..bc62b431f --- /dev/null +++ b/client/src/test/java/io/split/engine/sse/utils/DecompressionUtilTest.java @@ -0,0 +1,36 @@ +package io.split.engine.sse.utils; + +import org.junit.Assert; +import org.junit.Test; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Base64; +import java.util.zip.DataFormatException; + +import static io.split.engine.sse.utils.DecompressionUtil.gZipDecompress; +import static io.split.engine.sse.utils.DecompressionUtil.zLibDecompress; + +public class DecompressionUtilTest { + + + @Test + public void testZLibDecompress() throws UnsupportedEncodingException, DataFormatException { + String toDecode = "eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU="; + + byte[] decodedBytes = Base64.getDecoder().decode(toDecode); + byte[] decompressFeatureFlag = zLibDecompress(decodedBytes); + String featureFlag = new String(decompressFeatureFlag, 0, decompressFeatureFlag.length, "UTF-8"); + Assert.assertEquals("{\"trafficTypeName\":\"user\",\"id\":\"d431cdd0-b0be-11ea-8a80-1660ada9ce39\",\"name\":\"mauro_java\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-92391491,\"seed\":-1769377604,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1684265694505,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"WHITELIST\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"matcherType\":\"WHITELIST\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"admin\",\"mauro\",\"nico\"]}}]},\"partitions\":[{\"treatment\":\"v5\",\"size\":100}],\"label\":\"whitelisted\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"maur-2\"}}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100},{\"treatment\":\"V4\",\"size\":0},{\"treatment\":\"v5\",\"size\":0}],\"label\":\"in segment maur-2\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100},{\"treatment\":\"V4\",\"size\":0},{\"treatment\":\"v5\",\"size\":0}],\"label\":\"default rule\"}]}", featureFlag); + } + + @Test + public void testGZipDecompress() throws IOException { + String toDecode = "H4sIAAAAAAAA/8yT327aTBDFXyU612vJxoTgvUMfKB8qcaSapqoihAZ7DNusvWi9TpUiv3tl/pdQVb1qL+cwc3bOj/EGzlKeq3T6tuaYCoZEXbGFgMogkXXDIM0y31v4C/aCgMnrU9/3gl7Pp4yilMMIAuVusqDamvlXeiWIg/FAa5OSU6aEDHz/ip4wZ5Be1AmjoBsFAtVOCO56UXh31/O7ApUjV1eQGPw3HT+NIPCitG7bctIVC2ScU63d1DK5gksHCZPnEEhXVC45rosFW8ig1++GYej3g85tJEB6aSA7Aqkpc7Ws7XahCnLTbLVM7evnzalsUUHi8//j6WgyTqYQKMilK7b31tRryLa3WKiyfRCDeHhq2Dntiys+JS/J8THUt5VyrFXlHnYTQ3LU2h91yGdQVqhy+0RtTeuhUoNZ08wagTVZdxbBndF5vYVApb7z9m9pZgKaFqwhT+6coRHvg398nEweP/157Bd+S1hz6oxtm88O73B0jbhgM47nyej+YRRfgdNODDlXJWcJL9tUF5SqnRqfbtPr4LdcTHnk4rfp3buLOkG7+Pmp++vRM9w/wVblzX7Pm8OGfxf5YDKZfxh9SS6B/2Pc9t/7ja01o5k1PwIAAP//uTipVskEAAA="; + + byte[] decodedBytes = Base64.getDecoder().decode(toDecode); + byte[] decompressFeatureFlag = gZipDecompress(decodedBytes); + String featureFlag = new String(decompressFeatureFlag, 0, decompressFeatureFlag.length, "UTF-8"); + Assert.assertEquals("{\"trafficTypeName\":\"user\",\"id\":\"d431cdd0-b0be-11ea-8a80-1660ada9ce39\",\"name\":\"mauro_java\",\"trafficAllocation\":100,\"trafficAllocationSeed\":-92391491,\"seed\":-1769377604,\"status\":\"ACTIVE\",\"killed\":false,\"defaultTreatment\":\"off\",\"changeNumber\":1684333081259,\"algo\":2,\"configurations\":{},\"conditions\":[{\"conditionType\":\"WHITELIST\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"matcherType\":\"WHITELIST\",\"negate\":false,\"whitelistMatcherData\":{\"whitelist\":[\"admin\",\"mauro\",\"nico\"]}}]},\"partitions\":[{\"treatment\":\"v5\",\"size\":100}],\"label\":\"whitelisted\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"IN_SEGMENT\",\"negate\":false,\"userDefinedSegmentMatcherData\":{\"segmentName\":\"maur-2\"}}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100},{\"treatment\":\"V4\",\"size\":0},{\"treatment\":\"v5\",\"size\":0}],\"label\":\"in segment maur-2\"},{\"conditionType\":\"ROLLOUT\",\"matcherGroup\":{\"combiner\":\"AND\",\"matchers\":[{\"keySelector\":{\"trafficType\":\"user\"},\"matcherType\":\"ALL_KEYS\",\"negate\":false}]},\"partitions\":[{\"treatment\":\"on\",\"size\":0},{\"treatment\":\"off\",\"size\":100},{\"treatment\":\"V4\",\"size\":0},{\"treatment\":\"v5\",\"size\":0}],\"label\":\"default rule\"}]}", featureFlag); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java b/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java new file mode 100644 index 000000000..09dd66b38 --- /dev/null +++ b/client/src/test/java/io/split/engine/sse/workers/FeatureFlagWorkerImpTest.java @@ -0,0 +1,74 @@ +package io.split.engine.sse.workers; + +import io.split.client.utils.Json; +import io.split.engine.common.Synchronizer; +import io.split.engine.common.SynchronizerImp; +import io.split.engine.experiments.SplitParser; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.engine.sse.dtos.RawMessageNotification; +import io.split.storages.SplitCacheProducer; +import io.split.storages.memory.InMemoryCacheImp; +import io.split.telemetry.domain.UpdatesFromSSE; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class FeatureFlagWorkerImpTest { + + @Test + public void testRefreshSplitsWithCorrectFF() { + SplitParser splitParser = new SplitParser(); + Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, telemetryRuntimeProducer); + String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505,\\\"pcn\\\":0,\\\"c\\\":2,\\\"d\\\":\\\"eJzMk99u2kwQxV8lOtdryQZj8N6hD5QPlThSTVNVEUKDPYZt1jZar1OlyO9emf8lVFWv2ss5zJyd82O8hTWUZSqZvW04opwhUVdsIKBSSKR+10vS1HWW7pIdz2NyBjRwHS8IXEopTLgbQqDYT+ZUm3LxlV4J4mg81LpMyKqygPRc94YeM6eQTtjphp4fegLVXvD6Qdjt9wPXF6gs2bqCxPC/2eRpDIEXpXXblpGuWCDljGptZ4bJ5lxYSJRZBoFkTcWKozpfsoH0goHfCXpB6PfcngDpVQnZEUjKIlOr2uwWqiC3zU5L1aF+3p7LFhUkPv8/mY2nk3gGgZxssmZzb8p6A9n25ktVtA9iGI3ODXunQ3HDp+AVWT6F+rZWlrWq7MN+YkSWWvuTDvkMSnNV7J6oTdl6qKTEvGnmjcCGjL2IYC/ovPYgUKnvvPtbmrmApiVryLM7p2jE++AfH6fTx09/HvuF32LWnNjStM0Xh3c8ukZcsZlEi3h8/zCObsBpJ0acqYLTmFdtqitK1V6NzrfpdPBbLmVx4uK26e27izpDu/r5yf/16AXun2Cr4u6w591xw7+LfDidLj6Mv8TXwP8xbofv/c7UmtHMmx8BAAD//0fclvU=\\\"}\"}"; + RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); + GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); + FeatureFlagChangeNotification featureFlagChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); + featureFlagsWorker.executeRefresh(featureFlagChangeNotification); + UpdatesFromSSE updatesFromSSE = telemetryRuntimeProducer.popUpdatesFromSSE(); + Assert.assertEquals(1, updatesFromSSE.getSplits()); + Mockito.verify(synchronizer, Mockito.times(0)).refreshSplits(1684265694505L); + Mockito.verify(synchronizer, Mockito.times(1)).forceRefreshSegment(Mockito.anyString()); + } + + @Test + public void testRefreshSplitsWithEmptyData() { + SplitParser splitParser = new SplitParser(); + Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, telemetryRuntimeProducer); + String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1684265694505}\"}"; + RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); + GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); + FeatureFlagChangeNotification featureFlagChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); + featureFlagsWorker.executeRefresh(featureFlagChangeNotification); + UpdatesFromSSE updatesFromSSE = telemetryRuntimeProducer.popUpdatesFromSSE(); + Assert.assertEquals(0, updatesFromSSE.getSplits()); + Mockito.verify(synchronizer, Mockito.times(1)).refreshSplits(1684265694505L); + Mockito.verify(synchronizer, Mockito.times(0)).forceRefreshSegment(Mockito.anyString()); + } + + @Test + public void testRefreshSplitsArchiveFF() { + SplitParser splitParser = new SplitParser(); + Synchronizer synchronizer = Mockito.mock(SynchronizerImp.class); + SplitCacheProducer splitCacheProducer = new InMemoryCacheImp(1686165614090L); + TelemetryStorage telemetryRuntimeProducer = new InMemoryTelemetryStorage(); + FeatureFlagWorkerImp featureFlagsWorker = new FeatureFlagWorkerImp(synchronizer, splitParser, splitCacheProducer, telemetryRuntimeProducer); + String notification = "{\"id\":\"vQQ61wzBRO:0:0\",\"clientId\":\"pri:MTUxNzg3MDg1OQ==\",\"timestamp\":1684265694676,\"encoding\":\"json\",\"channel\":\"NzM2MDI5Mzc0_MjkyNTIzNjczMw==_splits\",\"data\":\"{\\\"type\\\":\\\"SPLIT_UPDATE\\\",\\\"changeNumber\\\":1686165617166,\\\"pcn\\\":1686165614090,\\\"c\\\":2,\\\"d\\\":\\\"eJxsUdFu4jAQ/JVqnx3JDjTh/JZCrj2JBh0EqtOBIuNswKqTIMeuxKH8+ykhiKrqiyXvzM7O7lzAGlEUSqbnEyaiRODgGjRAQOXAIQ/puPB96tHHIPQYQ/QmFNErxEgG44DKnI2AQHXtTOI0my6WcXZAmxoUtsTKvil7nNZVoQ5RYdFERh7VBwK5TY60rqWwqq6AM0q/qa8Qc+As/EHZ5HHMCDR9wQ/9kIajcEygscK6BjhEy+nLr008AwLvSuuOVgjdIIEcC+H03RZw2Hg/n88JEJBHUR0wceUeDXAWTAIWPAYsZEFAQOhDDdwnIPslnOk9NcAvNwEOly3IWtdmC3wLe+1wCy0Q2Hh/zNvTV9xg3sFtr5irQe3v5f7twgAOy8V8vlinQKAUVh7RPJvanbrBsi73qurMQpTM7oSrzjueV6hR2tp05E8J39MV1hq1d7YrWWxsZ2cQGYjzeLXK0pcoyRbLLP69juZZuuiyxoPo2oa7ukqYc+JKNEq+XgVmwopucC6sGMSS9etTvAQCH0I7BO7Ttt21BE7C2E8XsN+l06h/CJy25CveH/eGM0rbHQEt9qiHnR62jtKR7N/8wafQ7tr/AQAA//8S4fPB\\\"}\"}"; + RawMessageNotification rawMessageNotification = Json.fromJson(notification, RawMessageNotification.class); + GenericNotificationData genericNotificationData = Json.fromJson(rawMessageNotification.getData(), GenericNotificationData.class); + FeatureFlagChangeNotification featureFlagChangeNotification = new FeatureFlagChangeNotification(genericNotificationData); + featureFlagsWorker.executeRefresh(featureFlagChangeNotification); + UpdatesFromSSE updatesFromSSE = telemetryRuntimeProducer.popUpdatesFromSSE(); + Assert.assertEquals(1, updatesFromSSE.getSplits()); + Mockito.verify(synchronizer, Mockito.times(0)).refreshSplits(1686165617166L); + Mockito.verify(synchronizer, Mockito.times(0)).forceRefreshSegment(Mockito.anyString()); + } +} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java b/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java index 8db49054b..d8a6811a5 100644 --- a/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java +++ b/client/src/test/java/io/split/engine/sse/workers/SplitsWorkerTest.java @@ -1,6 +1,13 @@ package io.split.engine.sse.workers; import io.split.engine.common.Synchronizer; +import io.split.engine.experiments.SplitParser; +import io.split.engine.sse.dtos.FeatureFlagChangeNotification; +import io.split.engine.sse.dtos.GenericNotificationData; +import io.split.engine.sse.dtos.SplitKillNotification; +import io.split.storages.SplitCacheProducer; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -15,70 +22,103 @@ public class SplitsWorkerTest { @Test public void addToQueueWithoutElementsWShouldNotTriggerFetch() throws InterruptedException { Synchronizer splitFetcherMock = Mockito.mock(Synchronizer.class); + SplitParser splitParser = new SplitParser(); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); - SplitsWorker splitsWorker = new SplitsWorkerImp(splitFetcherMock); - splitsWorker.start(); + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(splitFetcherMock, splitParser, splitCacheProducer, telemetryRuntimeProducer); + featureFlagsWorker.start(); Thread.sleep(500); - Mockito.verify(splitFetcherMock, Mockito.never()).refreshSplits(Mockito.anyLong()); - splitsWorker.stop(); + Mockito.verify(splitFetcherMock, Mockito.never()).refreshSplits(Mockito.anyObject()); + featureFlagsWorker.stop(); } @Test public void addToQueueWithElementsWShouldTriggerFetch() throws InterruptedException { Synchronizer syncMock = Mockito.mock(Synchronizer.class); + SplitParser splitParser = new SplitParser(); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); - SplitsWorker splitsWorker = new SplitsWorkerImp(syncMock); - splitsWorker.start(); + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, splitCacheProducer, telemetryRuntimeProducer); + featureFlagsWorker.start(); ArgumentCaptor cnCaptor = ArgumentCaptor.forClass(Long.class); - splitsWorker.addToQueue(1585956698457L); - splitsWorker.addToQueue(1585956698467L); - splitsWorker.addToQueue(1585956698477L); - splitsWorker.addToQueue(1585956698476L); + + featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(1585956698457L) + .build())); + featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(1585956698467L) + .build())); + featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(1585956698477L) + .build())); + featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(1585956698476L) + .build())); Thread.sleep(1000); Mockito.verify(syncMock, Mockito.times(4)).refreshSplits(cnCaptor.capture()); List captured = cnCaptor.getAllValues(); assertThat(captured, contains(1585956698457L, 1585956698467L, 1585956698477L, 1585956698476L)); - splitsWorker.stop(); + featureFlagsWorker.stop(); } @Test public void killShouldTriggerFetch() { long changeNumber = 1585956698457L; - String splitName = "split-test"; + String featureFlagName = "feature-flag-test"; String defaultTreatment = "off"; Synchronizer syncMock = Mockito.mock(Synchronizer.class); - SplitsWorker splitsWorker = new SplitsWorkerImp(syncMock); - splitsWorker.start(); - - splitsWorker.killSplit(changeNumber, splitName, defaultTreatment); - Mockito.verify(syncMock, Mockito.times(1)).localKillSplit(splitName, defaultTreatment, changeNumber); - splitsWorker.stop(); + SplitParser splitParser = new SplitParser(); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, splitCacheProducer, telemetryRuntimeProducer) { + }; + featureFlagsWorker.start(); + SplitKillNotification splitKillNotification = new SplitKillNotification(GenericNotificationData.builder() + .changeNumber(changeNumber) + .defaultTreatment(defaultTreatment) + .featureFlagName(featureFlagName) + .build()); + + featureFlagsWorker.kill(splitKillNotification); + Mockito.verify(syncMock, Mockito.times(1)).localKillSplit(splitKillNotification); + featureFlagsWorker.stop(); } @Test public void messagesNotProcessedWhenWorkerStopped() throws InterruptedException { Synchronizer syncMock = Mockito.mock(Synchronizer.class); - SplitsWorker splitsWorker = new SplitsWorkerImp(syncMock); - splitsWorker.start(); - splitsWorker.addToQueue(1585956698457L); + SplitParser splitParser = new SplitParser(); + SplitCacheProducer splitCacheProducer = Mockito.mock(SplitCacheProducer.class); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); + FeatureFlagsWorker featureFlagsWorker = new FeatureFlagWorkerImp(syncMock, splitParser, splitCacheProducer, telemetryRuntimeProducer); + featureFlagsWorker.start(); + featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(1585956698457L) + .build())); Thread.sleep(500); - splitsWorker.stop(); + featureFlagsWorker.stop(); Thread.sleep(500); - splitsWorker.addToQueue(1585956698467L); - Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(1585956698457L); // Previous one! + featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(1585956698467L) + .build())); + Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(Mockito.anyObject()); // Previous one! Mockito.reset(syncMock); - splitsWorker.start(); - splitsWorker.addToQueue(1585956698477L); + featureFlagsWorker.start(); + featureFlagsWorker.addToQueue(new FeatureFlagChangeNotification(GenericNotificationData.builder() + .changeNumber(1585956698477L) + .build())); Thread.sleep(500); - Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(1585956698477L); - splitsWorker.stop(); + Mockito.verify(syncMock, Mockito.times(1)).refreshSplits(Mockito.anyObject()); + featureFlagsWorker.stop(); } -} +} \ No newline at end of file diff --git a/client/src/test/java/io/split/telemetry/storage/InMemoryTelemetryStorageTest.java b/client/src/test/java/io/split/telemetry/storage/InMemoryTelemetryStorageTest.java index 9f4986ad1..5bfc3b5d3 100644 --- a/client/src/test/java/io/split/telemetry/storage/InMemoryTelemetryStorageTest.java +++ b/client/src/test/java/io/split/telemetry/storage/InMemoryTelemetryStorageTest.java @@ -1,7 +1,21 @@ package io.split.telemetry.storage; -import io.split.telemetry.domain.*; -import io.split.telemetry.domain.enums.*; + +import io.split.telemetry.domain.HTTPErrors; +import io.split.telemetry.domain.HTTPLatencies; +import io.split.telemetry.domain.LastSynchronization; +import io.split.telemetry.domain.MethodExceptions; +import io.split.telemetry.domain.MethodLatencies; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.UpdatesFromSSE; + +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.MethodEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.domain.enums.UpdatesFromSSEEnum; import org.junit.Assert; import org.junit.Test; @@ -10,7 +24,7 @@ public class InMemoryTelemetryStorageTest{ @Test - public void testInMemoryTelemetryStorage() throws Exception { + public void testInMemoryTelemetryStorage() { InMemoryTelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); //MethodLatencies @@ -215,5 +229,12 @@ public void testInMemoryTelemetryStorage() throws Exception { tags = telemetryStorage.popTags(); Assert.assertEquals(0, tags.size()); + //UpdatesFromSSE + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); + + UpdatesFromSSE updatesFromSSE = telemetryStorage.popUpdatesFromSSE(); + Assert.assertEquals(3, updatesFromSSE.getSplits()); } -} +} \ No newline at end of file diff --git a/client/src/test/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitterTest.java b/client/src/test/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitterTest.java index 42913f5cd..e0dffb152 100644 --- a/client/src/test/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitterTest.java +++ b/client/src/test/java/io/split/telemetry/synchronizer/TelemetryInMemorySubmitterTest.java @@ -2,7 +2,6 @@ import io.split.TestHelper; import io.split.client.dtos.UniqueKeys; -import io.split.client.impressions.UniqueKeysTrackerImp; import io.split.storages.SegmentCacheConsumer; import io.split.storages.SplitCacheConsumer; import io.split.client.ApiKeyCounter; @@ -73,7 +72,7 @@ public void testSynchronizeUniqueKeys() throws Exception { } @Test - public void testConfig() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException, URISyntaxException, NoSuchFieldException, ClassNotFoundException { + public void testConfig() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException, URISyntaxException, NoSuchFieldException { ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); @@ -154,9 +153,10 @@ public void testStats() throws Exception { Assert.assertEquals(290, streamingEvents.get(0).get_data()); Assert.assertEquals(1, streamingEvents.get(0).get_type()); Assert.assertEquals(91218, streamingEvents.get(0).getTimestamp()); + Assert.assertEquals(1, stats.get_updatesFromSSE().getSplits()); } - private TelemetryInMemorySubmitter getTelemetrySynchronizer(CloseableHttpClient httpClient) throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException { + private TelemetryInMemorySubmitter getTelemetrySynchronizer(CloseableHttpClient httpClient) throws URISyntaxException { TelemetryStorageConsumer consumer = Mockito.mock(InMemoryTelemetryStorage.class); TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); SplitCacheConsumer splitCacheConsumer = Mockito.mock(SplitCacheConsumer.class); @@ -229,6 +229,7 @@ private void populateStats(TelemetryStorage telemetryStorage) { StreamingEvent streamingEvent = new StreamingEvent(1, 290, 91218); telemetryStorage.recordStreamingEvents(streamingEvent); + telemetryStorage.recordUpdatesFromSSE(UpdatesFromSSEEnum.SPLITS); } private void populateConfig(TelemetryStorage telemetryStorage) { @@ -238,5 +239,4 @@ private void populateConfig(TelemetryStorage telemetryStorage) { telemetryStorage.recordNonReadyUsage(); telemetryStorage.recordNonReadyUsage(); } - } \ No newline at end of file diff --git a/pluggable-storage/pom.xml b/pluggable-storage/pom.xml index 973356e62..240e626f5 100644 --- a/pluggable-storage/pom.xml +++ b/pluggable-storage/pom.xml @@ -6,7 +6,7 @@ java-client-parent io.split.client - 4.7.2 + 4.8.0 2.0.0 diff --git a/pom.xml b/pom.xml index bcdb9e209..502fdce4b 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.split.client java-client-parent - 4.7.2 + 4.8.0 diff --git a/redis-wrapper/pom.xml b/redis-wrapper/pom.xml index d91cee285..2e6c6da58 100644 --- a/redis-wrapper/pom.xml +++ b/redis-wrapper/pom.xml @@ -6,7 +6,7 @@ java-client-parent io.split.client - 4.7.2 + 4.8.0 redis-wrapper 3.0.0 diff --git a/testing/pom.xml b/testing/pom.xml index 8d146fd11..b75f26bd4 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -5,7 +5,7 @@ io.split.client java-client-parent - 4.7.2 + 4.8.0 java-client-testing jar