diff --git a/README.md b/README.md index 2771f2a1c..c361a6894 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ Split has built and maintains SDKs for: * Java [Github](https://github.com/splitio/java-client) [Docs](https://help.split.io/hc/en-us/articles/360020405151-Java-SDK) * Javascript [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK) * Node [Github](https://github.com/splitio/javascript-client) [Docs](https://help.split.io/hc/en-us/articles/360020564931-Node-js-SDK) -* .NET [Github](https://github.com/splitio/.net-core-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) +* .NET [Github](https://github.com/splitio/dotnet-client) [Docs](https://help.split.io/hc/en-us/articles/360020240172--NET-SDK) * Ruby [Github](https://github.com/splitio/ruby-client) [Docs](https://help.split.io/hc/en-us/articles/360020673251-Ruby-SDK) * PHP [Github](https://github.com/splitio/php-client) [Docs](https://help.split.io/hc/en-us/articles/360020350372-PHP-SDK) * Python [Github](https://github.com/splitio/python-client) [Docs](https://help.split.io/hc/en-us/articles/360020359652-Python-SDK) diff --git a/client/CHANGES.txt b/client/CHANGES.txt index e02d816d8..2d413bf6e 100644 --- a/client/CHANGES.txt +++ b/client/CHANGES.txt @@ -1,10 +1,15 @@ CHANGES +4.2.0 (Jun 7, 2021) +- Updated SDK telemetry storage, metrics and updater to be more effective and send less often. +- Improved the synchronization flow to be more reliable in the event of an edge case generating delay in cache purge propagation, keeping the SDK cache properly synced. +- Fixed issue where the SDK was validating no Split had over 50 conditions (legacy code). + 4.1.6 (Apr 15, 2021) --Updated log level and message in some messages. +- Updated log level and message in some messages. 4.1.5 (Apr 6, 2021) --Updated: Streaming retry fix. +- Updated streaming logic to use limited fetch retry attempts. 4.1.4 (Mar 19, 2021) - Updated: Internal cache structure refactor. diff --git a/client/pom.xml b/client/pom.xml index 5336cf310..15019f068 100644 --- a/client/pom.xml +++ b/client/pom.xml @@ -5,7 +5,7 @@ io.split.client java-client-parent - 4.1.6 + 4.2.0 java-client jar @@ -121,7 +121,7 @@ com.google.guava guava - 29.0-jre + 30.0-jre org.slf4j diff --git a/client/src/main/java/io/split/cache/SegmentCache.java b/client/src/main/java/io/split/cache/SegmentCache.java index d75fe11d3..81349a5b9 100644 --- a/client/src/main/java/io/split/cache/SegmentCache.java +++ b/client/src/main/java/io/split/cache/SegmentCache.java @@ -1,6 +1,9 @@ package io.split.cache; +import io.split.engine.segments.SegmentImp; + import java.util.List; +import java.util.Set; /** * Memory for segments @@ -42,4 +45,16 @@ public interface SegmentCache { * clear all segments */ void clear(); + + /** + * return every segment + * @return + */ + List getAll(); + + /** + * return key count + * @return + */ + long getKeyCount(); } diff --git a/client/src/main/java/io/split/cache/SegmentCacheInMemoryImpl.java b/client/src/main/java/io/split/cache/SegmentCacheInMemoryImpl.java index 0c705c016..774c8d20f 100644 --- a/client/src/main/java/io/split/cache/SegmentCacheInMemoryImpl.java +++ b/client/src/main/java/io/split/cache/SegmentCacheInMemoryImpl.java @@ -6,7 +6,9 @@ import org.slf4j.LoggerFactory; import java.util.List; +import java.util.Set; import java.util.concurrent.ConcurrentMap; +import java.util.stream.Collectors; /** * InMemoryCache Implementation @@ -59,4 +61,14 @@ public long getChangeNumber(String segmentName) { public void clear() { _segments.clear(); } + + @Override + public List getAll() { + return _segments.values().stream().collect(Collectors.toList()); + } + + @Override + public long getKeyCount() { + return _segments.values().stream().mapToLong(SegmentImp::getKeysSize).sum(); + } } diff --git a/client/src/main/java/io/split/client/ApiKeyCounter.java b/client/src/main/java/io/split/client/ApiKeyCounter.java index 8c39394dd..426ade17b 100644 --- a/client/src/main/java/io/split/client/ApiKeyCounter.java +++ b/client/src/main/java/io/split/client/ApiKeyCounter.java @@ -6,6 +6,9 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.HashMap; +import java.util.Map; + public class ApiKeyCounter { private static final Logger _log = LoggerFactory.getLogger(ApiKeyCounter.class); @@ -63,4 +66,18 @@ boolean isApiKeyPresent(String apiKey) { int getCount(String apiKey) { return USED_API_KEYS.count(apiKey); } + + public Map getFactoryInstances() { + Map factoryInstances = new HashMap<>(); + for (String factory :USED_API_KEYS) { + factoryInstances.putIfAbsent(factory, new Long(getCount(factory))); + } + + return factoryInstances; + } + + @VisibleForTesting + void clearApiKeys() { + USED_API_KEYS.clear(); + } } diff --git a/client/src/main/java/io/split/client/EventClientImpl.java b/client/src/main/java/io/split/client/EventClientImpl.java index 5c3ae8c13..eb4da7703 100644 --- a/client/src/main/java/io/split/client/EventClientImpl.java +++ b/client/src/main/java/io/split/client/EventClientImpl.java @@ -4,6 +4,11 @@ import io.split.client.dtos.Event; import io.split.client.utils.GenericClientUtil; import io.split.client.utils.Utils; +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.storage.TelemetryEvaluationProducer; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -23,6 +28,7 @@ import java.util.concurrent.TimeUnit; import static java.lang.Thread.MIN_PRIORITY; +import static com.google.common.base.Preconditions.checkNotNull; /** * Responsible for sending events added via .track() to Split collection services @@ -45,34 +51,28 @@ public class EventClientImpl implements EventClient { private final CloseableHttpClient _httpclient; private final URI _target; private final int _waitBeforeShutdown; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; ThreadFactory eventClientThreadFactory(final String name) { - return new ThreadFactory() { - @Override - public Thread newThread(final Runnable r) { - return new Thread(new Runnable() { - @Override - public void run() { - Thread.currentThread().setPriority(MIN_PRIORITY); - r.run(); - } - }, name); - } - }; + return r -> new Thread(() -> { + Thread.currentThread().setPriority(MIN_PRIORITY); + r.run(); + }, name); } - public static EventClientImpl create(CloseableHttpClient httpclient, URI eventsRootTarget, int maxQueueSize, long flushIntervalMillis, int waitBeforeShutdown) throws URISyntaxException { - return new EventClientImpl(new LinkedBlockingQueue(), + public static EventClientImpl create(CloseableHttpClient httpclient, URI eventsRootTarget, int maxQueueSize, long flushIntervalMillis, int waitBeforeShutdown, TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { + return new EventClientImpl(new LinkedBlockingQueue<>(maxQueueSize), httpclient, Utils.appendPath(eventsRootTarget, "api/events/bulk"), maxQueueSize, flushIntervalMillis, - waitBeforeShutdown); + waitBeforeShutdown, + telemetryRuntimeProducer); } EventClientImpl(BlockingQueue eventQueue, CloseableHttpClient httpclient, URI target, int maxQueueSize, - long flushIntervalMillis, int waitBeforeShutdown) throws URISyntaxException { + long flushIntervalMillis, int waitBeforeShutdown, TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { _httpclient = httpclient; @@ -83,6 +83,7 @@ public static EventClientImpl create(CloseableHttpClient httpclient, URI eventsR _maxQueueSize = maxQueueSize; _flushIntervalMillis = flushIntervalMillis; + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); _senderExecutor = new ThreadPoolExecutor( 1, @@ -122,9 +123,16 @@ public boolean track(Event event, int eventSize) { if (event == null) { return false; } - _eventQueue.put(new WrappedEvent(event, eventSize)); + if(_eventQueue.offer(new WrappedEvent(event, eventSize))) { + _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 1); + } + else { + _log.warn("Event dropped."); + _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1); + } - } catch (InterruptedException e) { + } catch (ClassCastException | NullPointerException | IllegalArgumentException e) { + _telemetryRuntimeProducer.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1); _log.warn("Interruption when adding event withed while adding message %s.", event); return false; } @@ -153,7 +161,7 @@ public void run() { List events = new ArrayList<>(); long accumulated = 0; try { - while (true) { + while (!Thread.currentThread().isInterrupted()) { WrappedEvent data = _eventQueue.take(); Event event = data.event(); Long size = data.size(); @@ -169,7 +177,7 @@ public void run() { continue; } - + long initTime = System.currentTimeMillis(); if (events.size() >= _maxQueueSize || accumulated >= MAX_SIZE_BYTES || event == CENTINEL) { // Send over the network @@ -183,6 +191,8 @@ public void run() { // Clear the queue of events for the next batch. events = new ArrayList<>(); accumulated = 0; + _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.EVENTS, System.currentTimeMillis()-initTime); + _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.EVENTS, System.currentTimeMillis()); } } } catch (InterruptedException e) { diff --git a/client/src/main/java/io/split/client/HttpSegmentChangeFetcher.java b/client/src/main/java/io/split/client/HttpSegmentChangeFetcher.java index 7d7d735f6..a9475320f 100644 --- a/client/src/main/java/io/split/client/HttpSegmentChangeFetcher.java +++ b/client/src/main/java/io/split/client/HttpSegmentChangeFetcher.java @@ -4,11 +4,18 @@ import io.split.client.dtos.SegmentChange; import io.split.client.utils.Json; import io.split.client.utils.Utils; +import io.split.engine.common.FetchOptions; import io.split.engine.metrics.Metrics; import io.split.engine.segments.SegmentChangeFetcher; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.net.URIBuilder; import org.slf4j.Logger; @@ -17,6 +24,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -27,57 +36,72 @@ public final class HttpSegmentChangeFetcher implements SegmentChangeFetcher { private static final Logger _log = LoggerFactory.getLogger(HttpSegmentChangeFetcher.class); private static final String SINCE = "since"; + private static final String TILL = "till"; private static final String PREFIX = "segmentChangeFetcher"; - private static final String NAME_CACHE = "Cache-Control"; - private static final String VALUE_CACHE = "no-cache"; + private static final String CACHE_CONTROL_HEADER_NAME = "Cache-Control"; + private static final String CACHE_CONTROL_HEADER_VALUE = "no-cache"; + + private static final String HEADER_FASTLY_DEBUG_NAME = "Fastly-Debug"; + private static final String HEADER_FASTLY_DEBUG_VALUE = "1"; private final CloseableHttpClient _client; private final URI _target; - private final Metrics _metrics; - - public static HttpSegmentChangeFetcher create(CloseableHttpClient client, URI root) throws URISyntaxException { - return create(client, root, new Metrics.NoopMetrics()); - } + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; - public static HttpSegmentChangeFetcher create(CloseableHttpClient client, URI root, Metrics metrics) throws URISyntaxException { - return new HttpSegmentChangeFetcher(client, Utils.appendPath(root, "api/segmentChanges"), metrics); + public static HttpSegmentChangeFetcher create(CloseableHttpClient client, URI root, TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { + return new HttpSegmentChangeFetcher(client, Utils.appendPath(root, "api/segmentChanges"), telemetryRuntimeProducer); } - private HttpSegmentChangeFetcher(CloseableHttpClient client, URI uri, Metrics metrics) { + private HttpSegmentChangeFetcher(CloseableHttpClient client, URI uri, TelemetryRuntimeProducer telemetryRuntimeProducer) { _client = client; _target = uri; - _metrics = metrics; checkNotNull(_target); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } @Override - public SegmentChange fetch(String segmentName, long since, boolean addCacheHeader) { + public SegmentChange fetch(String segmentName, long since, FetchOptions options) { long start = System.currentTimeMillis(); CloseableHttpResponse response = null; try { String path = _target.getPath() + "/" + segmentName; - URI uri = new URIBuilder(_target).setPath(path).addParameter(SINCE, "" + since).build(); + URIBuilder uriBuilder = new URIBuilder(_target) + .setPath(path) + .addParameter(SINCE, "" + since); + if (options.hasCustomCN()) { + uriBuilder.addParameter(TILL, "" + options.targetCN()); + } + + URI uri = uriBuilder.build(); HttpGet request = new HttpGet(uri); - if(addCacheHeader) { - request.setHeader(NAME_CACHE, VALUE_CACHE); + + if(options.cacheControlHeadersEnabled()) { + request.setHeader(CACHE_CONTROL_HEADER_NAME, CACHE_CONTROL_HEADER_VALUE); + } + + if (options.fastlyDebugHeaderEnabled()) { + request.addHeader(HEADER_FASTLY_DEBUG_NAME, HEADER_FASTLY_DEBUG_VALUE); } + response = _client.execute(request); + options.handleResponseHeaders(Arrays.stream(response.getHeaders()) + .collect(Collectors.toMap(Header::getName, Header::getValue))); int statusCode = response.getCode(); - if (statusCode < 200 || statusCode >= 300) { + if (statusCode < HttpStatus.SC_OK || statusCode >= HttpStatus.SC_MULTIPLE_CHOICES) { + _telemetryRuntimeProducer.recordSyncError(ResourceEnum.SEGMENT_SYNC, statusCode); _log.error("Response status was: " + statusCode); - if (statusCode == 403) { + if (statusCode == HttpStatus.SC_FORBIDDEN) { _log.error("factory instantiation: you passed a browser type api_key, " + "please grab an api key from the Split console that is of type sdk"); } - _metrics.count(PREFIX + ".status." + statusCode, 1); throw new IllegalStateException("Could not retrieve segment changes for " + segmentName + "; http return code " + statusCode); } - + _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SEGMENTS, System.currentTimeMillis()); String json = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); if (_log.isDebugEnabled()) { @@ -86,11 +110,10 @@ public SegmentChange fetch(String segmentName, long since, boolean addCacheHeade return Json.fromJson(json, SegmentChange.class); } catch (Throwable t) { - _metrics.count(PREFIX + ".exception", 1); throw new IllegalStateException("Problem fetching segmentChanges: " + t.getMessage(), t); } finally { + _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.SEGMENTS, System.currentTimeMillis()-start); Utils.forceClose(response); - _metrics.time(PREFIX + ".time", System.currentTimeMillis() - start); } diff --git a/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java b/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java index 3c5f9b8fc..9d77654e5 100644 --- a/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java +++ b/client/src/main/java/io/split/client/HttpSplitChangeFetcher.java @@ -4,11 +4,18 @@ import io.split.client.dtos.SplitChange; import io.split.client.utils.Json; import io.split.client.utils.Utils; +import io.split.engine.common.FetchOptions; import io.split.engine.experiments.SplitChangeFetcher; import io.split.engine.metrics.Metrics; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.net.URIBuilder; import org.slf4j.Logger; @@ -17,6 +24,8 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkNotNull; @@ -27,49 +36,66 @@ public final class HttpSplitChangeFetcher implements SplitChangeFetcher { private static final Logger _log = LoggerFactory.getLogger(HttpSplitChangeFetcher.class); private static final String SINCE = "since"; + private static final String TILL = "till"; private static final String PREFIX = "splitChangeFetcher"; - private static final String NAME_CACHE = "Cache-Control"; - private static final String VALUE_CACHE = "no-cache"; + + private static final String HEADER_CACHE_CONTROL_NAME = "Cache-Control"; + private static final String HEADER_CACHE_CONTROL_VALUE = "no-cache"; + + private static final String HEADER_FASTLY_DEBUG_NAME = "Fastly-Debug"; + private static final String HEADER_FASTLY_DEBUG_VALUE = "1"; private final CloseableHttpClient _client; private final URI _target; - private final Metrics _metrics; - - public static HttpSplitChangeFetcher create(CloseableHttpClient client, URI root) throws URISyntaxException { - return create(client, root, new Metrics.NoopMetrics()); - } + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; - public static HttpSplitChangeFetcher create(CloseableHttpClient client, URI root, Metrics metrics) throws URISyntaxException { - return new HttpSplitChangeFetcher(client, Utils.appendPath(root, "api/splitChanges"), metrics); + public static HttpSplitChangeFetcher create(CloseableHttpClient client, URI root, TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { + return new HttpSplitChangeFetcher(client, Utils.appendPath(root, "api/splitChanges"), telemetryRuntimeProducer); } - private HttpSplitChangeFetcher(CloseableHttpClient client, URI uri, Metrics metrics) { + private HttpSplitChangeFetcher(CloseableHttpClient client, URI uri, TelemetryRuntimeProducer telemetryRuntimeProducer) { _client = client; _target = uri; - _metrics = metrics; checkNotNull(_target); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + } + + long makeRandomTill() { + + return (-1)*(long)Math.floor(Math.random()*(Math.pow(2, 63))); } @Override - public SplitChange fetch(long since, boolean addCacheHeader) { + public SplitChange fetch(long since, FetchOptions options) { long start = System.currentTimeMillis(); CloseableHttpResponse response = null; try { - URI uri = new URIBuilder(_target).addParameter(SINCE, "" + since).build(); + URIBuilder uriBuilder = new URIBuilder(_target).addParameter(SINCE, "" + since); + if (options.hasCustomCN()) { + uriBuilder.addParameter(TILL, "" + options.targetCN()); + } + URI uri = uriBuilder.build(); HttpGet request = new HttpGet(uri); - if(addCacheHeader) { - request.setHeader(NAME_CACHE, VALUE_CACHE); + if(options.cacheControlHeadersEnabled()) { + request.setHeader(HEADER_CACHE_CONTROL_NAME, HEADER_CACHE_CONTROL_VALUE); } + + if (options.fastlyDebugHeaderEnabled()) { + request.addHeader(HEADER_FASTLY_DEBUG_NAME, HEADER_FASTLY_DEBUG_VALUE); + } + response = _client.execute(request); + options.handleResponseHeaders(Arrays.stream(response.getHeaders()) + .collect(Collectors.toMap(Header::getName, Header::getValue))); int statusCode = response.getCode(); - if (statusCode < 200 || statusCode >= 300) { - _metrics.count(PREFIX + ".status." + statusCode, 1); + if (statusCode < HttpStatus.SC_OK || statusCode >= HttpStatus.SC_MULTIPLE_CHOICES) { + _telemetryRuntimeProducer.recordSyncError(ResourceEnum.SPLIT_SYNC, statusCode); throw new IllegalStateException("Could not retrieve splitChanges; http return code " + statusCode); } @@ -81,11 +107,10 @@ public SplitChange fetch(long since, boolean addCacheHeader) { return Json.fromJson(json, SplitChange.class); } catch (Throwable t) { - _metrics.count(PREFIX + ".exception", 1); throw new IllegalStateException("Problem fetching splitChanges: " + t.getMessage(), t); } finally { + _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.SPLITS, System.currentTimeMillis()-start); Utils.forceClose(response); - _metrics.time(PREFIX + ".time", System.currentTimeMillis() - start); } } diff --git a/client/src/main/java/io/split/client/LocalhostSplitFactory.java b/client/src/main/java/io/split/client/LocalhostSplitFactory.java index 0ec01f8c9..3a2f0a14b 100644 --- a/client/src/main/java/io/split/client/LocalhostSplitFactory.java +++ b/client/src/main/java/io/split/client/LocalhostSplitFactory.java @@ -6,6 +6,8 @@ import io.split.engine.SDKReadinessGates; import io.split.engine.evaluator.EvaluatorImp; import io.split.engine.metrics.Metrics; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.NoopTelemetryStorage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,12 +56,12 @@ public LocalhostSplitFactory(String directory, String file) throws IOException { SplitCache splitCache = new InMemoryCacheImp(); SDKReadinessGates sdkReadinessGates = new SDKReadinessGates(); - sdkReadinessGates.splitsAreReady(); _cacheUpdaterService = new CacheUpdaterService(splitCache); _cacheUpdaterService.updateCache(splitAndKeyToTreatment); + sdkReadinessGates.sdkInternalReady(); _client = new SplitClientImpl(this, splitCache, - new ImpressionsManager.NoOpImpressionsManager(), new Metrics.NoopMetrics(), new NoopEventClient(), - SplitClientConfig.builder().setBlockUntilReadyTimeout(1).build(), sdkReadinessGates, new EvaluatorImp(splitCache)); + new ImpressionsManager.NoOpImpressionsManager(), new NoopEventClient(), + SplitClientConfig.builder().setBlockUntilReadyTimeout(1).build(), sdkReadinessGates, new EvaluatorImp(splitCache), new NoopTelemetryStorage(), new NoopTelemetryStorage()); _manager = LocalhostSplitManager.of(splitAndKeyToTreatment); _splitFile.registerWatcher(); diff --git a/client/src/main/java/io/split/client/SplitClientConfig.java b/client/src/main/java/io/split/client/SplitClientConfig.java index cafd52ebb..7b2ae0e5b 100644 --- a/client/src/main/java/io/split/client/SplitClientConfig.java +++ b/client/src/main/java/io/split/client/SplitClientConfig.java @@ -17,6 +17,11 @@ public class SplitClientConfig { public static final String LOCALHOST_DEFAULT_FILE = "split.yaml"; + public static final String SDK_ENDPOINT = "https://sdk.split.io"; + public static final String EVENTS_ENDPOINT = "https://events.split.io"; + public static final String AUTH_ENDPOINT = "https://auth.split.io/api/auth"; + public static final String STREAMING_ENDPOINT = "https://streaming.split.io/sse"; + public static final String TELEMETRY_ENDPOINT = "https://telemetry.split.io/api/v1"; private final String _endpoint; private final String _eventsEndpoint; @@ -46,6 +51,12 @@ public class SplitClientConfig { private final int _streamingReconnectBackoffBase; private final String _authServiceURL; private final String _streamingServiceURL; + private final String _telemetryURL; + private final int _telemetryRefreshRate; + private final int _onDemandFetchRetryDelayMs; + private final int _onDemandFetchMaxRetries; + private final int _failedAttemptsBeforeLogging; + private final boolean _cdnDebugLogging; // Proxy configs private final HttpHost _proxy; @@ -89,7 +100,13 @@ private SplitClientConfig(String endpoint, int authRetryBackoffBase, int streamingReconnectBackoffBase, String authServiceURL, - String streamingServiceURL) { + String streamingServiceURL, + String telemetryURL, + int telemetryRefreshRate, + int onDemandFetchRetryDelayMs, + int onDemandFetchMaxRetries, + int failedAttemptsBeforeLogging, + boolean cdnDebugLogging) { _endpoint = endpoint; _eventsEndpoint = eventsEndpoint; _featuresRefreshRate = pollForFeatureChangesEveryNSeconds; @@ -120,6 +137,12 @@ private SplitClientConfig(String endpoint, _streamingReconnectBackoffBase = streamingReconnectBackoffBase; _authServiceURL = authServiceURL; _streamingServiceURL = streamingServiceURL; + _telemetryURL = telemetryURL; + _telemetryRefreshRate = telemetryRefreshRate; + _onDemandFetchRetryDelayMs = onDemandFetchRetryDelayMs; + _onDemandFetchMaxRetries = onDemandFetchMaxRetries; + _failedAttemptsBeforeLogging = failedAttemptsBeforeLogging; + _cdnDebugLogging = cdnDebugLogging; Properties props = new Properties(); try { @@ -248,11 +271,27 @@ public String streamingServiceURL() { return _streamingServiceURL; } + public String telemetryURL() { + return _telemetryURL; + } + + public int get_telemetryRefreshRate() { + return _telemetryRefreshRate; + } + public int streamingRetryDelay() {return _onDemandFetchRetryDelayMs;} + + public int streamingFetchMaxRetries() {return _onDemandFetchMaxRetries;} + + public int failedAttemptsBeforeLogging() {return _failedAttemptsBeforeLogging;} + + public boolean cdnDebugLogging() { return _cdnDebugLogging; } + + public static final class Builder { - private String _endpoint = "https://sdk.split.io"; + private String _endpoint = SDK_ENDPOINT; private boolean _endpointSet = false; - private String _eventsEndpoint = "https://events.split.io"; + private String _eventsEndpoint = EVENTS_ENDPOINT; private boolean _eventsEndpointSet = false; private int _featuresRefreshRate = 60; private int _segmentsRefreshRate = 60; @@ -281,8 +320,14 @@ public static final class Builder { private boolean _streamingEnabled = true; private int _authRetryBackoffBase = 1; private int _streamingReconnectBackoffBase = 1; - private String _authServiceURL = "https://auth.split.io/api/auth"; - private String _streamingServiceURL = "https://streaming.split.io/sse"; + private String _authServiceURL = AUTH_ENDPOINT; + private String _streamingServiceURL = STREAMING_ENDPOINT; + private String _telemetryURl = TELEMETRY_ENDPOINT; + private int _telemetryRefreshRate = 60; + private int _onDemandFetchRetryDelayMs = 50; + private final int _onDemandFetchMaxRetries = 10; + private final int _failedAttemptsBeforeLogging = 10; + private final boolean _cdnDebugLogging = true; public Builder() { } @@ -674,6 +719,27 @@ public Builder streamingServiceURL(String streamingServiceURL) { return this; } + /** + * Set telemetry service URL. + * @param telemetryURL + * @return + */ + public Builder telemetryURL(String telemetryURL) { + _telemetryURl = telemetryURL; + return this; + } + + /** + * How often send telemetry data + * + * @param telemetryRefreshRate + * @return this builder + */ + public Builder telemetryRefreshRate(int telemetryRefreshRate) { + _telemetryRefreshRate = telemetryRefreshRate; + return this; + } + public SplitClientConfig build() { if (_featuresRefreshRate < 5 ) { throw new IllegalArgumentException("featuresRefreshRate must be >= 5: " + _featuresRefreshRate); @@ -744,6 +810,17 @@ public SplitClientConfig build() { throw new IllegalArgumentException("streamingServiceURL must not be null"); } + if (_telemetryURl == null) { + throw new IllegalArgumentException("telemetryURl must not be null"); + } + + if (_onDemandFetchRetryDelayMs <= 0) { + throw new IllegalStateException("streamingRetryDelay must be > 0"); + } + if(_onDemandFetchMaxRetries <= 0) { + throw new IllegalStateException("_onDemandFetchMaxRetries must be > 0"); + } + return new SplitClientConfig( _endpoint, _eventsEndpoint, @@ -774,7 +851,13 @@ public SplitClientConfig build() { _authRetryBackoffBase, _streamingReconnectBackoffBase, _authServiceURL, - _streamingServiceURL); + _streamingServiceURL, + _telemetryURl, + _telemetryRefreshRate, + _onDemandFetchRetryDelayMs, + _onDemandFetchMaxRetries, + _failedAttemptsBeforeLogging, + _cdnDebugLogging); } } } diff --git a/client/src/main/java/io/split/client/SplitClientImpl.java b/client/src/main/java/io/split/client/SplitClientImpl.java index 7792f5a1f..7427165ab 100644 --- a/client/src/main/java/io/split/client/SplitClientImpl.java +++ b/client/src/main/java/io/split/client/SplitClientImpl.java @@ -10,12 +10,14 @@ import io.split.engine.evaluator.Evaluator; import io.split.engine.evaluator.EvaluatorImp; import io.split.engine.evaluator.Labels; -import io.split.engine.metrics.Metrics; import io.split.grammar.Treatments; import io.split.inputValidation.EventsValidator; import io.split.inputValidation.KeyValidator; import io.split.inputValidation.SplitNameValidator; import io.split.inputValidation.TrafficTypeValidator; +import io.split.telemetry.domain.enums.MethodEnum; +import io.split.telemetry.storage.TelemetryConfigProducer; +import io.split.telemetry.storage.TelemetryEvaluationProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,36 +37,36 @@ public final class SplitClientImpl implements SplitClient { public static final SplitResult SPLIT_RESULT_CONTROL = new SplitResult(Treatments.CONTROL, null); - private static final String GET_TREATMENT = "getTreatment"; - private static final String GET_TREATMENT_WITH_CONFIG = "getTreatmentWithConfig"; - private static final Logger _log = LoggerFactory.getLogger(SplitClientImpl.class); private final SplitFactory _container; private final SplitCache _splitCache; private final ImpressionsManager _impressionManager; - private final Metrics _metrics; private final SplitClientConfig _config; private final EventClient _eventClient; private final SDKReadinessGates _gates; private final Evaluator _evaluator; + private final TelemetryEvaluationProducer _telemetryEvaluationProducer; + private final TelemetryConfigProducer _telemetryConfigProducer; public SplitClientImpl(SplitFactory container, SplitCache splitCache, ImpressionsManager impressionManager, - Metrics metrics, EventClient eventClient, SplitClientConfig config, SDKReadinessGates gates, - Evaluator evaluator) { + Evaluator evaluator, + TelemetryEvaluationProducer telemetryEvaluationProducer, + TelemetryConfigProducer telemetryConfigProducer) { _container = container; _splitCache = checkNotNull(splitCache); _impressionManager = checkNotNull(impressionManager); - _metrics = metrics; _eventClient = eventClient; _config = config; _gates = checkNotNull(gates); _evaluator = checkNotNull(evaluator); + _telemetryEvaluationProducer = checkNotNull(telemetryEvaluationProducer); + _telemetryConfigProducer = checkNotNull(telemetryConfigProducer); } @Override @@ -74,27 +76,27 @@ public String getTreatment(String key, String split) { @Override public String getTreatment(String key, String split, Map attributes) { - return getTreatmentWithConfigInternal(GET_TREATMENT, key, null, split, attributes).treatment(); + return getTreatmentWithConfigInternal(key, null, split, attributes, MethodEnum.TREATMENT).treatment(); } @Override public String getTreatment(Key key, String split, Map attributes) { - return getTreatmentWithConfigInternal(GET_TREATMENT, key.matchingKey(), key.bucketingKey(), split, attributes).treatment(); + return getTreatmentWithConfigInternal(key.matchingKey(), key.bucketingKey(), split, attributes, MethodEnum.TREATMENT).treatment(); } @Override public SplitResult getTreatmentWithConfig(String key, String split) { - return getTreatmentWithConfigInternal(GET_TREATMENT_WITH_CONFIG, key, null, split, Collections.emptyMap()); + return getTreatmentWithConfigInternal(key, null, split, Collections.emptyMap(), MethodEnum.TREATMENT_WITH_CONFIG); } @Override public SplitResult getTreatmentWithConfig(String key, String split, Map attributes) { - return getTreatmentWithConfigInternal(GET_TREATMENT_WITH_CONFIG, key, null, split, attributes); + return getTreatmentWithConfigInternal(key, null, split, attributes, MethodEnum.TREATMENT_WITH_CONFIG); } @Override public SplitResult getTreatmentWithConfig(Key key, String split, Map attributes) { - return getTreatmentWithConfigInternal(GET_TREATMENT_WITH_CONFIG, key.matchingKey(), key.bucketingKey(), split, attributes); + return getTreatmentWithConfigInternal(key.matchingKey(), key.bucketingKey(), split, attributes, MethodEnum.TREATMENT_WITH_CONFIG); } @Override @@ -132,7 +134,7 @@ public void blockUntilReady() throws TimeoutException, InterruptedException { if (_config.blockUntilReady() <= 0) { throw new IllegalArgumentException("setBlockUntilReadyTimeout must be positive but in config was: " + _config.blockUntilReady()); } - if (!_gates.isSDKReady(_config.blockUntilReady())) { + if (!_gates.waitUntilInternalReady(_config.blockUntilReady())) { throw new TimeoutException("SDK was not ready in " + _config.blockUntilReady()+ " milliseconds"); } _log.debug(String.format("Split SDK ready in %d ms", (System.currentTimeMillis() - startTime))); @@ -144,6 +146,7 @@ public void destroy() { } private boolean track(Event event) { + long initTime = System.currentTimeMillis(); if (_container.isDestroyed()) { _log.error("Client has already been destroyed - no calls possible"); return false; @@ -173,26 +176,32 @@ private boolean track(Event event) { } event.properties = propertiesResult.getValue(); + _telemetryEvaluationProducer.recordLatency(MethodEnum.TRACK, System.currentTimeMillis() - initTime); return _eventClient.track(event, propertiesResult.getEventSize()); } - private SplitResult getTreatmentWithConfigInternal(String method, String matchingKey, String bucketingKey, String split, Map attributes) { + private SplitResult getTreatmentWithConfigInternal(String matchingKey, String bucketingKey, String split, Map attributes, MethodEnum methodEnum) { + long initTime = System.currentTimeMillis(); try { + if(!_gates.isSDKReady()){ + _log.warn(methodEnum.getMethod() + ": the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method"); + _telemetryConfigProducer.recordNonReadyUsage(); + } if (_container.isDestroyed()) { _log.error("Client has already been destroyed - no calls possible"); return SPLIT_RESULT_CONTROL; } - if (!KeyValidator.isValid(matchingKey, "matchingKey", _config.maxStringLength(), method)) { + if (!KeyValidator.isValid(matchingKey, "matchingKey", _config.maxStringLength(), methodEnum.getMethod())) { return SPLIT_RESULT_CONTROL; } - if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), method)) { + if (!KeyValidator.bucketingKeyIsValid(bucketingKey, _config.maxStringLength(), methodEnum.getMethod())) { return SPLIT_RESULT_CONTROL; } - Optional splitNameResult = SplitNameValidator.isValid(split, method); + Optional splitNameResult = SplitNameValidator.isValid(split, methodEnum.getMethod()); if (!splitNameResult.isPresent()) { return SPLIT_RESULT_CONTROL; } @@ -202,7 +211,7 @@ private SplitResult getTreatmentWithConfigInternal(String method, String matchin EvaluatorImp.TreatmentLabelAndChangeNumber result = _evaluator.evaluateFeature(matchingKey, bucketingKey, split, attributes); - if (result.treatment.equals(Treatments.CONTROL) && result.label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReadyNow()) { + if (result.treatment.equals(Treatments.CONTROL) && result.label.equals(Labels.DEFINITION_NOT_FOUND) && _gates.isSDKReady()) { _log.warn( "getTreatment: you passed \"" + split + "\" that does not exist in this environment, " + "please double check what Splits exist in the web console."); @@ -214,15 +223,16 @@ private SplitResult getTreatmentWithConfigInternal(String method, String matchin split, start, result.treatment, - String.format("sdk.%s", method), + String.format("sdk.%s", methodEnum.getMethod()), _config.labelsEnabled() ? result.label : null, result.changeNumber, attributes ); - + _telemetryEvaluationProducer.recordLatency(methodEnum, System.currentTimeMillis()-initTime); return new SplitResult(result.treatment, result.configurations); } catch (Exception e) { try { + _telemetryEvaluationProducer.recordException(methodEnum); _log.error("CatchAll Exception", e); } catch (Exception e1) { // ignore @@ -235,7 +245,6 @@ private void recordStats(String matchingKey, String bucketingKey, String split, String operation, String label, Long changeNumber, Map attributes) { try { _impressionManager.track(new Impression(matchingKey, bucketingKey, split, result, System.currentTimeMillis(), label, changeNumber, attributes)); - _metrics.time(operation, System.currentTimeMillis() - start); } catch (Throwable t) { _log.error("Exception", t); } diff --git a/client/src/main/java/io/split/client/SplitFactoryBuilder.java b/client/src/main/java/io/split/client/SplitFactoryBuilder.java index f18032416..f7f1fea8b 100644 --- a/client/src/main/java/io/split/client/SplitFactoryBuilder.java +++ b/client/src/main/java/io/split/client/SplitFactoryBuilder.java @@ -66,7 +66,7 @@ public static SplitFactory local(SplitClientConfig config) throws IOException, U return LocalhostSplitFactory.createLocalhostSplitFactory(config); } - public static void main(String... args) throws IOException, InterruptedException, TimeoutException, URISyntaxException { + public static void main(String... args) throws IOException, URISyntaxException { if (args.length != 1) { System.out.println("Usage: "); System.exit(1); diff --git a/client/src/main/java/io/split/client/SplitFactoryImpl.java b/client/src/main/java/io/split/client/SplitFactoryImpl.java index 8b576d0dd..1489bafec 100644 --- a/client/src/main/java/io/split/client/SplitFactoryImpl.java +++ b/client/src/main/java/io/split/client/SplitFactoryImpl.java @@ -3,12 +3,11 @@ import io.split.client.impressions.AsynchronousImpressionListener; import io.split.client.impressions.ImpressionListener; import io.split.client.impressions.ImpressionsManagerImpl; -import io.split.client.interceptors.AddSplitHeadersFilter; +import io.split.client.interceptors.AuthorizationInterceptorFilter; +import io.split.client.interceptors.ClientKeyInterceptorFilter; import io.split.client.interceptors.GzipDecoderResponseInterceptor; import io.split.client.interceptors.GzipEncoderRequestInterceptor; -import io.split.client.metrics.CachedMetrics; -import io.split.client.metrics.FireAndForgetMetrics; -import io.split.client.metrics.HttpMetrics; +import io.split.client.interceptors.SdkMetadataInterceptorFilter; import io.split.cache.InMemoryCacheImp; import io.split.cache.SplitCache; import io.split.engine.evaluator.Evaluator; @@ -26,6 +25,11 @@ import io.split.cache.SegmentCacheInMemoryImpl; import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.integrations.IntegrationsConfig; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import io.split.telemetry.synchronizer.TelemetrySubmitter; +import io.split.telemetry.synchronizer.TelemetrySyncTask; +import io.split.telemetry.synchronizer.TelemetrySynchronizer; import org.apache.hc.client5.http.auth.AuthScope; import org.apache.hc.client5.http.auth.Credentials; import org.apache.hc.client5.http.auth.UsernamePasswordCredentials; @@ -50,10 +54,10 @@ import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; + import java.util.ArrayList; import java.util.List; import java.util.Random; -import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; public class SplitFactoryImpl implements SplitFactory { @@ -67,13 +71,10 @@ public class SplitFactoryImpl implements SplitFactory { private final URI _eventsRootTarget; private final CloseableHttpClient _httpclient; private final SDKReadinessGates _gates; - private final HttpMetrics _httpMetrics; - private final FireAndForgetMetrics _unCachedFireAndForget; private final SegmentSynchronizationTaskImp _segmentSynchronizationTaskImp; private final SplitFetcher _splitFetcher; private final SplitSynchronizationTask _splitSynchronizationTask; private final ImpressionsManagerImpl _impressionsManager; - private final FireAndForgetMetrics _cachedFireAndForgetMetrics; private final EventClient _eventClient; private final SyncManager _syncManager; private final Evaluator _evaluator; @@ -89,12 +90,19 @@ public class SplitFactoryImpl implements SplitFactory { private boolean isTerminated = false; private final ApiKeyCounter _apiKeyCounter; + private final TelemetryStorage _telemetryStorage; + private final TelemetrySynchronizer _telemetrySynchronizer; + private final TelemetrySyncTask _telemetrySyncTask; + private final long _startTime; public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyntaxException { + _startTime = System.currentTimeMillis(); _apiToken = apiToken; _apiKeyCounter = ApiKeyCounter.getApiKeyCounterInstance(); _apiKeyCounter.add(apiToken); + _telemetryStorage = new InMemoryTelemetryStorage(); + if (config.blockUntilReady() == -1) { //BlockUntilReady not been set _log.warn("no setBlockUntilReadyTimeout parameter has been set - incorrect control treatments could be logged” " + @@ -112,15 +120,11 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn _rootTarget = URI.create(config.endpoint()); _eventsRootTarget = URI.create(config.eventsEndpoint()); - // HttpMetrics - _httpMetrics = HttpMetrics.create(_httpclient, _eventsRootTarget); - // Cache Initialisations _segmentCache = new SegmentCacheInMemoryImpl(); _splitCache = new InMemoryCacheImp(); + _telemetrySynchronizer = new TelemetrySubmitter(_httpclient, URI.create(config.telemetryURL()), _telemetryStorage, _splitCache, _segmentCache, _telemetryStorage, _startTime); - // Metrics - _unCachedFireAndForget = FireAndForgetMetrics.instance(_httpMetrics, 2, 1000); // Segments _segmentSynchronizationTaskImp = buildSegments(config); @@ -129,36 +133,66 @@ public SplitFactoryImpl(String apiToken, SplitClientConfig config) throws URISyn _splitFetcher = buildSplitFetcher(); // SplitSynchronizationTask - _splitSynchronizationTask = new SplitSynchronizationTask(_splitFetcher, _splitCache, findPollingPeriod(RANDOM, config.featuresRefreshRate())); + _splitSynchronizationTask = new SplitSynchronizationTask(_splitFetcher, + _splitCache, + findPollingPeriod(RANDOM, config.featuresRefreshRate())); // Impressions _impressionsManager = buildImpressionsManager(config); - // CachedFireAndForgetMetrics - _cachedFireAndForgetMetrics = buildCachedFireAndForgetMetrics(config); - // EventClient - _eventClient = EventClientImpl.create(_httpclient, _eventsRootTarget, config.eventsQueueSize(), config.eventFlushIntervalInMillis(), config.waitBeforeShutdown()); + _eventClient = EventClientImpl.create(_httpclient, + _eventsRootTarget, + config.eventsQueueSize(), + config.eventFlushIntervalInMillis(), + config.waitBeforeShutdown(), + _telemetryStorage); - // SyncManager - _syncManager = SyncManagerImp.build(config.streamingEnabled(), _splitSynchronizationTask, _splitFetcher, _segmentSynchronizationTaskImp, _splitCache, config.authServiceURL(), _httpclient, config.streamingServiceURL(), config.authRetryBackoffBase(), buildSSEdHttpClient(config), _segmentCache); - _syncManager.start(); + _telemetrySyncTask = new TelemetrySyncTask(config.get_telemetryRefreshRate(), _telemetrySynchronizer); // Evaluator _evaluator = new EvaluatorImp(_splitCache); // SplitClient - _client = new SplitClientImpl(this, _splitCache, _impressionsManager, _cachedFireAndForgetMetrics, _eventClient, config, _gates, _evaluator); + _client = new SplitClientImpl(this, + _splitCache, + _impressionsManager, + _eventClient, + config, + _gates, + _evaluator, + _telemetryStorage, + _telemetryStorage); // SplitManager - _manager = new SplitManagerImpl(_splitCache, config, _gates); + _manager = new SplitManagerImpl(_splitCache, config, _gates, _telemetryStorage); + + // SyncManager + _syncManager = SyncManagerImp.build(config.streamingEnabled(), + _splitSynchronizationTask, + _splitFetcher, + _segmentSynchronizationTaskImp, + _splitCache, + config.authServiceURL(), + _httpclient, + config.streamingServiceURL(), + config.authRetryBackoffBase(), + buildSSEdHttpClient(apiToken, config), + _segmentCache, + config.streamingRetryDelay(), + config.streamingFetchMaxRetries(), + config.failedAttemptsBeforeLogging(), + config.cdnDebugLogging(), _gates, _telemetryStorage, _telemetrySynchronizer,config); + _syncManager.start(); // DestroyOnShutDown if (config.destroyOnShutDown()) { - Runtime.getRuntime().addShutdownHook(new Thread(() -> { + Thread shutdown = new Thread(() -> { // Using the full path to avoid conflicting with Thread.destroy() SplitFactoryImpl.this.destroy(); - })); + }); + shutdown.setName("split-destroy-worker"); + Runtime.getRuntime().addShutdownHook(shutdown); } } @@ -177,22 +211,24 @@ public synchronized void destroy() { if (!isTerminated) { _log.info("Shutdown called for split"); try { - _segmentSynchronizationTaskImp.close(); - _log.info("Successful shutdown of segment fetchers"); - _splitSynchronizationTask.close(); - _log.info("Successful shutdown of splits"); + long splitCount = _splitCache.getAll().stream().count(); + long segmentCount = _segmentCache.getAll().stream().count(); + long segmentKeyCount = _segmentCache.getKeyCount(); _impressionsManager.close(); _log.info("Successful shutdown of impressions manager"); - _unCachedFireAndForget.close(); - _log.info("Successful shutdown of metrics 1"); - _cachedFireAndForgetMetrics.close(); - _log.info("Successful shutdown of metrics 2"); - _httpclient.close(); - _log.info("Successful shutdown of httpclient"); _eventClient.close(); _log.info("Successful shutdown of eventClient"); + _segmentSynchronizationTaskImp.close(); + _log.info("Successful shutdown of segment fetchers"); + _splitSynchronizationTask.close(); + _log.info("Successful shutdown of splits"); _syncManager.shutdown(); _log.info("Successful shutdown of syncManager"); + _telemetryStorage.recordSessionLength(System.currentTimeMillis() - _startTime); + _telemetrySyncTask.stopScheduledTask(splitCount, segmentCount, segmentKeyCount); + _log.info("Successful shutdown of telemetry sync task"); + _httpclient.close(); + _log.info("Successful shutdown of httpclient"); } catch (IOException e) { _log.error("We could not shutdown split", e); } @@ -207,7 +243,6 @@ public boolean isDestroyed() { } private static CloseableHttpClient buildHttpClient(String apiToken, SplitClientConfig config) { - SSLConnectionSocketFactory sslSocketFactory = SSLConnectionSocketFactoryBuilder.create() .setSslContext(SSLContexts.createSystemDefault()) .setTlsVersions(TLS.V_1_1, TLS.V_1_2) @@ -230,7 +265,8 @@ private static CloseableHttpClient buildHttpClient(String apiToken, SplitClientC HttpClientBuilder httpClientbuilder = HttpClients.custom() .setConnectionManager(cm) .setDefaultRequestConfig(requestConfig) - .addRequestInterceptorLast(AddSplitHeadersFilter.instance(apiToken, config.ipAddressEnabled())) + .addRequestInterceptorLast(AuthorizationInterceptorFilter.instance(apiToken)) + .addRequestInterceptorLast(SdkMetadataInterceptorFilter.instance(config.ipAddressEnabled(), SplitClientConfig.splitSdkVersion)) .addRequestInterceptorLast(new GzipEncoderRequestInterceptor()) .addResponseInterceptorLast((new GzipDecoderResponseInterceptor())); @@ -242,7 +278,7 @@ private static CloseableHttpClient buildHttpClient(String apiToken, SplitClientC return httpClientbuilder.build(); } - private static CloseableHttpClient buildSSEdHttpClient(SplitClientConfig config) { + private static CloseableHttpClient buildSSEdHttpClient(String apiToken, SplitClientConfig config) { RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(Timeout.ofMilliseconds(SSE_CONNECT_TIMEOUT)) .build(); @@ -263,7 +299,9 @@ private static CloseableHttpClient buildSSEdHttpClient(SplitClientConfig config) HttpClientBuilder httpClientbuilder = HttpClients.custom() .setConnectionManager(cm) - .setDefaultRequestConfig(requestConfig); + .setDefaultRequestConfig(requestConfig) + .addRequestInterceptorLast(SdkMetadataInterceptorFilter.instance(config.ipAddressEnabled(), SplitClientConfig.splitSdkVersion)) + .addRequestInterceptorLast(ClientKeyInterceptorFilter.instance(apiToken)); // Set up proxy is it exists if (config.proxy() != null) { @@ -296,20 +334,21 @@ private static int findPollingPeriod(Random rand, int max) { } private SegmentSynchronizationTaskImp buildSegments(SplitClientConfig config) throws URISyntaxException { - SegmentChangeFetcher segmentChangeFetcher = HttpSegmentChangeFetcher.create(_httpclient, _rootTarget, _unCachedFireAndForget); + SegmentChangeFetcher segmentChangeFetcher = HttpSegmentChangeFetcher.create(_httpclient, _rootTarget, _telemetryStorage); return new SegmentSynchronizationTaskImp(segmentChangeFetcher, findPollingPeriod(RANDOM, config.segmentsRefreshRate()), config.numThreadsForSegmentFetch(), _gates, - _segmentCache); + _segmentCache, + _telemetryStorage); } private SplitFetcher buildSplitFetcher() throws URISyntaxException { - SplitChangeFetcher splitChangeFetcher = HttpSplitChangeFetcher.create(_httpclient, _rootTarget, _unCachedFireAndForget); + SplitChangeFetcher splitChangeFetcher = HttpSplitChangeFetcher.create(_httpclient, _rootTarget, _telemetryStorage); SplitParser splitParser = new SplitParser(_segmentSynchronizationTaskImp, _segmentCache); - return new SplitFetcherImp(splitChangeFetcher, splitParser, _gates, _splitCache); + return new SplitFetcherImp(splitChangeFetcher, splitParser, _splitCache, _telemetryStorage); } private ImpressionsManagerImpl buildImpressionsManager(SplitClientConfig config) throws URISyntaxException { @@ -324,12 +363,6 @@ private ImpressionsManagerImpl buildImpressionsManager(SplitClientConfig config) .collect(Collectors.toCollection(() -> impressionListeners)); } - return ImpressionsManagerImpl.instance(_httpclient, config, impressionListeners); - } - - private FireAndForgetMetrics buildCachedFireAndForgetMetrics(SplitClientConfig config) { - CachedMetrics cachedMetrics = new CachedMetrics(_httpMetrics, TimeUnit.SECONDS.toMillis(config.metricsRefreshRate())); - - return FireAndForgetMetrics.instance(cachedMetrics, 2, 1000); + return ImpressionsManagerImpl.instance(_httpclient, config, impressionListeners, _telemetryStorage); } } diff --git a/client/src/main/java/io/split/client/SplitManagerImpl.java b/client/src/main/java/io/split/client/SplitManagerImpl.java index 5304b5911..0edd88aaa 100644 --- a/client/src/main/java/io/split/client/SplitManagerImpl.java +++ b/client/src/main/java/io/split/client/SplitManagerImpl.java @@ -6,6 +6,7 @@ import io.split.cache.SplitCache; import io.split.engine.experiments.ParsedSplit; import io.split.inputValidation.SplitNameValidator; +import io.split.telemetry.storage.TelemetryConfigProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -26,18 +27,25 @@ public class SplitManagerImpl implements SplitManager { private final SplitCache _splitCache; private final SplitClientConfig _config; private final SDKReadinessGates _gates; + private final TelemetryConfigProducer _telemetryConfigProducer; public SplitManagerImpl(SplitCache splitCache, SplitClientConfig config, - SDKReadinessGates gates) { + SDKReadinessGates gates, + TelemetryConfigProducer telemetryConfigProducer) { _config = Preconditions.checkNotNull(config); _splitCache = Preconditions.checkNotNull(splitCache); _gates = Preconditions.checkNotNull(gates); + _telemetryConfigProducer = telemetryConfigProducer; } @Override public List splits() { + if (!_gates.isSDKReady()) { { + _log.warn("splits: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method"); + _telemetryConfigProducer.recordNonReadyUsage(); + }} List result = new ArrayList<>(); Collection parsedSplits = _splitCache.getAll(); for (ParsedSplit split : parsedSplits) { @@ -49,6 +57,10 @@ public List splits() { @Override public SplitView split(String featureName) { + if (!_gates.isSDKReady()) { { + _log.warn("split: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method"); + _telemetryConfigProducer.recordNonReadyUsage(); + }} Optional result = SplitNameValidator.isValid(featureName, "split"); if (!result.isPresent()) { return null; @@ -57,7 +69,7 @@ public SplitView split(String featureName) { ParsedSplit parsedSplit = _splitCache.get(featureName); if (parsedSplit == null) { - if (_gates.isSDKReadyNow()) { + if (_gates.isSDKReady()) { _log.warn("split: you passed \"" + featureName + "\" that does not exist in this environment, " + "please double check what Splits exist in the web console."); } @@ -69,6 +81,10 @@ public SplitView split(String featureName) { @Override public List splitNames() { + if (!_gates.isSDKReady()) { { + _log.warn("splitNames: the SDK is not ready, results may be incorrect. Make sure to wait for SDK readiness before using this method"); + _telemetryConfigProducer.recordNonReadyUsage(); + }} List result = new ArrayList<>(); Collection parsedSplits = _splitCache.getAll(); for (ParsedSplit split : parsedSplits) { @@ -83,7 +99,8 @@ public void blockUntilReady() throws TimeoutException, InterruptedException { if (_config.blockUntilReady() <= 0) { throw new IllegalArgumentException("setBlockUntilReadyTimeout must be positive but in config was: " + _config.blockUntilReady()); } - if (!_gates.isSDKReady(_config.blockUntilReady())) { + if (!_gates.waitUntilInternalReady(_config.blockUntilReady())) { + _telemetryConfigProducer.recordBURTimeout(); throw new TimeoutException("SDK was not ready in " + _config.blockUntilReady()+ " milliseconds"); } } diff --git a/client/src/main/java/io/split/client/YamlLocalhostSplitFile.java b/client/src/main/java/io/split/client/YamlLocalhostSplitFile.java index 926bab165..b9ece01c5 100644 --- a/client/src/main/java/io/split/client/YamlLocalhostSplitFile.java +++ b/client/src/main/java/io/split/client/YamlLocalhostSplitFile.java @@ -13,7 +13,7 @@ public class YamlLocalhostSplitFile extends AbstractLocalhostSplitFile { - private static final Logger _log = LoggerFactory.getLogger(LegacyLocalhostSplitFile.class); + private static final Logger _log = LoggerFactory.getLogger(YamlLocalhostSplitFile.class); public YamlLocalhostSplitFile(LocalhostSplitFactory localhostSplitFactory, String directory, String filenameYaml) throws IOException { super(localhostSplitFactory, directory, filenameYaml); diff --git a/client/src/main/java/io/split/client/impressions/AsynchronousImpressionListener.java b/client/src/main/java/io/split/client/impressions/AsynchronousImpressionListener.java index ed6a1e811..66f1d17d4 100644 --- a/client/src/main/java/io/split/client/impressions/AsynchronousImpressionListener.java +++ b/client/src/main/java/io/split/client/impressions/AsynchronousImpressionListener.java @@ -44,12 +44,7 @@ public AsynchronousImpressionListener(ImpressionListener delegate, ExecutorServi @Override public void log(final Impression impression) { try { - _executor.execute(new Runnable() { - @Override - public void run() { - _delegate.log(impression); - } - }); + _executor.execute(() -> _delegate.log(impression)); } catch (Exception e) { _log.warn("Unable to send impression to impression listener", e); diff --git a/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java b/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java index 336e7f2f5..017ef45a3 100644 --- a/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java +++ b/client/src/main/java/io/split/client/impressions/HttpImpressionsSender.java @@ -5,10 +5,15 @@ import io.split.client.dtos.TestImpressions; import io.split.client.utils.Utils; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpStatus; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,6 +23,8 @@ import java.util.HashMap; import java.util.List; +import static com.google.common.base.Preconditions.checkNotNull; + /** * Created by patricioe on 6/20/16. */ @@ -33,26 +40,29 @@ public class HttpImpressionsSender implements ImpressionsSender { private final URI _impressionBulkTarget; private final URI _impressionCountTarget; private final ImpressionsManager.Mode _mode; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; - public static HttpImpressionsSender create(CloseableHttpClient client, URI eventsRootEndpoint, ImpressionsManager.Mode mode) throws URISyntaxException { + public static HttpImpressionsSender create(CloseableHttpClient client, URI eventsRootEndpoint, ImpressionsManager.Mode mode, TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { return new HttpImpressionsSender(client, Utils.appendPath(eventsRootEndpoint, BULK_ENDPOINT_PATH), Utils.appendPath(eventsRootEndpoint, COUNT_ENDPOINT_PATH), - mode); + mode, + telemetryRuntimeProducer); } - private HttpImpressionsSender(CloseableHttpClient client, URI impressionBulkTarget, URI impressionCountTarget, ImpressionsManager.Mode mode) { + private HttpImpressionsSender(CloseableHttpClient client, URI impressionBulkTarget, URI impressionCountTarget, ImpressionsManager.Mode mode, TelemetryRuntimeProducer telemetryRuntimeProducer) { _client = client; _mode = mode; _impressionBulkTarget = impressionBulkTarget; _impressionCountTarget = impressionCountTarget; + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } @Override public void postImpressionsBulk(List impressions) { CloseableHttpResponse response = null; - + long initTime = System.currentTimeMillis(); try { HttpEntity entity = Utils.toJsonEntity(impressions); @@ -64,13 +74,16 @@ public void postImpressionsBulk(List impressions) { int status = response.getCode(); - if (status < 200 || status >= 300) { + if (status < HttpStatus.SC_OK || status >= HttpStatus.SC_MULTIPLE_CHOICES) { + _telemetryRuntimeProducer.recordSyncError(ResourceEnum.IMPRESSION_SYNC, status); _logger.warn("Response status was: " + status); } + _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS, System.currentTimeMillis()); } catch (Throwable t) { _logger.warn("Exception when posting impressions" + impressions, t); } finally { + _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.IMPRESSIONS, System.currentTimeMillis() - initTime); Utils.forceClose(response); } @@ -78,6 +91,7 @@ public void postImpressionsBulk(List impressions) { @Override public void postCounters(HashMap raw) { + long initTime = System.currentTimeMillis(); if (_mode.equals(ImpressionsManager.Mode.DEBUG)) { _logger.warn("Attempted to submit counters in impressions debugging mode. Ignoring"); return; @@ -87,9 +101,12 @@ public void postCounters(HashMap raw) { request.setEntity(Utils.toJsonEntity(ImpressionCount.fromImpressionCounterData(raw))); try (CloseableHttpResponse response = _client.execute(request)) { int status = response.getCode(); - if (status < 200 || status >= 300) { + if (status < HttpStatus.SC_OK || status >= HttpStatus.SC_MULTIPLE_CHOICES) { + _telemetryRuntimeProducer.recordSyncError(ResourceEnum.IMPRESSION_COUNT_SYNC, status); _logger.warn("Response status was: " + status); } + _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.IMPRESSIONS_COUNT, System.currentTimeMillis() - initTime); + _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS_COUNT, System.currentTimeMillis()); } catch (IOException exc) { _logger.warn("Exception when posting impression counters: ", exc); } diff --git a/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java b/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java index ef20caff6..e32455fdf 100644 --- a/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java +++ b/client/src/main/java/io/split/client/impressions/ImpressionsManagerImpl.java @@ -5,6 +5,8 @@ import io.split.client.SplitClientConfig; import io.split.client.dtos.KeyImpression; import io.split.client.dtos.TestImpressions; +import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,36 +43,41 @@ public class ImpressionsManagerImpl implements ImpressionsManager, Closeable { private final ImpressionCounter _counter; private final ImpressionListener _listener; private final ImpressionsManager.Mode _mode; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; public static ImpressionsManagerImpl instance(CloseableHttpClient client, SplitClientConfig config, - List listeners) throws URISyntaxException { - return new ImpressionsManagerImpl(client, config, null, listeners); + List listeners, + TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { + return new ImpressionsManagerImpl(client, config, null, listeners, telemetryRuntimeProducer); } public static ImpressionsManagerImpl instanceForTest(CloseableHttpClient client, SplitClientConfig config, ImpressionsSender impressionsSender, - List listeners) throws URISyntaxException { - return new ImpressionsManagerImpl(client, config, impressionsSender, listeners); + List listeners, + TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { + return new ImpressionsManagerImpl(client, config, impressionsSender, listeners, telemetryRuntimeProducer); } private ImpressionsManagerImpl(CloseableHttpClient client, SplitClientConfig config, ImpressionsSender impressionsSender, - List listeners) throws URISyntaxException { + List listeners, + TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { _config = checkNotNull(config); _mode = checkNotNull(config.impressionsMode()); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); _storage = new InMemoryImpressionsStorage(config.impressionsQueueSize()); _impressionObserver = new ImpressionObserver(LAST_SEEN_CACHE_SIZE); _counter = new ImpressionCounter(); _impressionsSender = (null != impressionsSender) ? impressionsSender - : HttpImpressionsSender.create(client, URI.create(config.eventsEndpoint()), _mode); + : HttpImpressionsSender.create(client, URI.create(config.eventsEndpoint()), _mode, telemetryRuntimeProducer); _scheduler = buildExecutor(); - _scheduler.scheduleAtFixedRate(this::sendImpressions, BULK_INITIAL_DELAY_SECONDS,config.impressionsRefreshRate(), TimeUnit.SECONDS); + _scheduler.scheduleAtFixedRate(this::sendImpressions, BULK_INITIAL_DELAY_SECONDS, config.impressionsRefreshRate(), TimeUnit.SECONDS); if (Mode.OPTIMIZED.equals(_mode)) { _scheduler.scheduleAtFixedRate(this::sendImpressionCounters, COUNT_INITIAL_DELAY_SECONDS, COUNT_REFRESH_RATE_SECONDS, TimeUnit.SECONDS); } @@ -97,9 +104,15 @@ public void track(Impression impression) { _counter.inc(impression.split(), impression.time(), 1); } - if (Mode.DEBUG.equals(_mode) || shouldQueueImpression(impression)) { - _storage.put(KeyImpression.fromImpression(impression)); + if (Mode.OPTIMIZED.equals(_mode) && !shouldQueueImpression(impression)) { + _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, 1); + return; + } + if (!_storage.put(KeyImpression.fromImpression(impression))) { + _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 1); + return; } + _telemetryRuntimeProducer.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_QUEUED, 1); } @Override @@ -129,14 +142,14 @@ public void close() { } _impressionsSender.postImpressionsBulk(TestImpressions.fromKeyImpressions(impressions)); - if(_config.debugEnabled()) { + if (_config.debugEnabled()) { _log.info(String.format("Posting %d Split impressions took %d millis", impressions.size(), (System.currentTimeMillis() - start))); } } @VisibleForTesting - /* package private */ void sendImpressionCounters() { + /* package private */ void sendImpressionCounters() { if (!_counter.isEmpty()) { _impressionsSender.postCounters(_counter.popAll()); } diff --git a/client/src/main/java/io/split/client/interceptors/AddSplitHeadersFilter.java b/client/src/main/java/io/split/client/interceptors/AddSplitHeadersFilter.java deleted file mode 100644 index 3367ac53d..000000000 --- a/client/src/main/java/io/split/client/interceptors/AddSplitHeadersFilter.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.split.client.interceptors; - -import io.split.client.SplitClientConfig; -import org.apache.hc.core5.http.EntityDetails; -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.HttpRequestInterceptor; -import org.apache.hc.core5.http.protocol.HttpContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.net.InetAddress; - -import static com.google.common.base.Preconditions.checkNotNull; - -/** - * Created by adilaijaz on 5/22/15. - */ -public class AddSplitHeadersFilter implements HttpRequestInterceptor { - private static final Logger _log = LoggerFactory.getLogger(AddSplitHeadersFilter.class); - - /* package private for testing purposes */ - static final String AUTHORIZATION_HEADER = "Authorization"; - static final String CLIENT_MACHINE_NAME_HEADER = "SplitSDKMachineName"; - static final String CLIENT_MACHINE_IP_HEADER = "SplitSDKMachineIP"; - static final String CLIENT_VERSION = "SplitSDKVersion"; - - private final String _apiTokenBearer; - private final String _hostname; - private final String _ip; - - public static AddSplitHeadersFilter instance(String apiToken, boolean ipAddressEnabled) { - if (!ipAddressEnabled) { - return new AddSplitHeadersFilter(apiToken, null, null); - } - - String hostname = null; - String ip = null; - - try { - InetAddress localHost = InetAddress.getLocalHost(); - hostname = localHost.getHostName(); - ip = localHost.getHostAddress(); - } catch (Exception e) { - _log.error("Could not resolve InetAddress", e); - } - - return new AddSplitHeadersFilter(apiToken, hostname, ip); - } - - private AddSplitHeadersFilter(String apiToken, String hostname, String ip) { - checkNotNull(apiToken); - - _apiTokenBearer = "Bearer " + apiToken; - _hostname = hostname; - _ip = ip; - } - - @Override - public void process(HttpRequest request, EntityDetails entity, HttpContext context) throws HttpException, IOException { - request.addHeader(AUTHORIZATION_HEADER, _apiTokenBearer); - request.addHeader(CLIENT_VERSION, SplitClientConfig.splitSdkVersion); - - if (_hostname != null) { - request.addHeader(CLIENT_MACHINE_NAME_HEADER, _hostname); - } - - if (_ip != null) { - request.addHeader(CLIENT_MACHINE_IP_HEADER, _ip); - } - } -} diff --git a/client/src/main/java/io/split/client/interceptors/AuthorizationInterceptorFilter.java b/client/src/main/java/io/split/client/interceptors/AuthorizationInterceptorFilter.java new file mode 100644 index 000000000..43b50949c --- /dev/null +++ b/client/src/main/java/io/split/client/interceptors/AuthorizationInterceptorFilter.java @@ -0,0 +1,30 @@ +package io.split.client.interceptors; + +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; + +import java.io.IOException; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class AuthorizationInterceptorFilter implements HttpRequestInterceptor { + static final String AUTHORIZATION_HEADER = "Authorization"; + + private final String _apiTokenBearer; + + public static AuthorizationInterceptorFilter instance(String apiToken) { + return new AuthorizationInterceptorFilter(apiToken); + } + + private AuthorizationInterceptorFilter(String apiToken) { + _apiTokenBearer = "Bearer " + checkNotNull(apiToken); + } + + @Override + public void process(HttpRequest httpRequest, EntityDetails entityDetails, HttpContext httpContext) throws HttpException, IOException { + httpRequest.addHeader(AUTHORIZATION_HEADER, _apiTokenBearer); + } +} diff --git a/client/src/main/java/io/split/client/interceptors/ClientKeyInterceptorFilter.java b/client/src/main/java/io/split/client/interceptors/ClientKeyInterceptorFilter.java new file mode 100644 index 000000000..ad3c82594 --- /dev/null +++ b/client/src/main/java/io/split/client/interceptors/ClientKeyInterceptorFilter.java @@ -0,0 +1,32 @@ +package io.split.client.interceptors; + +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; + +import java.io.IOException; + +public class ClientKeyInterceptorFilter implements HttpRequestInterceptor { + static final String CLIENT_KEY = "SplitSDKClientKey"; + + private final String _clientKey; + + public static ClientKeyInterceptorFilter instance(String apiToken) { + return new ClientKeyInterceptorFilter(getKey(apiToken)); + } + + private ClientKeyInterceptorFilter(String clientKey) { + _clientKey = clientKey; + } + + @Override + public void process(HttpRequest httpRequest, EntityDetails entityDetails, HttpContext httpContext) throws HttpException, IOException { + httpRequest.addHeader(CLIENT_KEY, _clientKey); + } + + private static String getKey(String clientKey) { + return clientKey.length() >4 ? clientKey.substring(clientKey.length() - 4) : clientKey; + } +} diff --git a/client/src/main/java/io/split/client/interceptors/SdkMetadataInterceptorFilter.java b/client/src/main/java/io/split/client/interceptors/SdkMetadataInterceptorFilter.java new file mode 100644 index 000000000..6cc059579 --- /dev/null +++ b/client/src/main/java/io/split/client/interceptors/SdkMetadataInterceptorFilter.java @@ -0,0 +1,61 @@ +package io.split.client.interceptors; + +import io.split.client.SplitClientConfig; +import org.apache.hc.core5.http.EntityDetails; +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.HttpRequestInterceptor; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.InetAddress; + +public class SdkMetadataInterceptorFilter implements HttpRequestInterceptor { + private static final Logger _log = LoggerFactory.getLogger(SdkMetadataInterceptorFilter.class); + + static final String CLIENT_MACHINE_NAME_HEADER = "SplitSDKMachineName"; + static final String CLIENT_MACHINE_IP_HEADER = "SplitSDKMachineIP"; + static final String CLIENT_VERSION = "SplitSDKVersion"; + + private final String _hostname; + private final String _ip; + private final String _sdkVersion; + + public static SdkMetadataInterceptorFilter instance(boolean ipAddressEnabled, String sdkVersion) { + String hostName = null; + String ip = null; + + if (ipAddressEnabled) { + try { + InetAddress localHost = InetAddress.getLocalHost(); + hostName = localHost.getHostName(); + ip = localHost.getHostAddress(); + } catch (Exception e) { + _log.error("Could not resolve InetAddress", e); + } + } + + return new SdkMetadataInterceptorFilter(hostName, ip, sdkVersion); + } + + private SdkMetadataInterceptorFilter(String hostName, String ip, String sdkVersion) { + _sdkVersion = sdkVersion; + _hostname = hostName; + _ip = ip; + } + + @Override + public void process(HttpRequest httpRequest, EntityDetails entityDetails, HttpContext httpContext) throws HttpException, IOException { + httpRequest.addHeader(CLIENT_VERSION, SplitClientConfig.splitSdkVersion); + + if (_hostname != null) { + httpRequest.addHeader(CLIENT_MACHINE_NAME_HEADER, _hostname); + } + + if (_ip != null) { + httpRequest.addHeader(CLIENT_MACHINE_IP_HEADER, _ip); + } + } +} 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 e5d49e115..0fcbea305 100644 --- a/client/src/main/java/io/split/client/jmx/SplitJmxMonitor.java +++ b/client/src/main/java/io/split/client/jmx/SplitJmxMonitor.java @@ -3,6 +3,7 @@ import io.split.cache.SegmentCache; import io.split.cache.SplitCache; import io.split.client.SplitClient; +import io.split.engine.common.FetchOptions; import io.split.engine.experiments.SplitFetcher; import io.split.engine.segments.SegmentFetcher; import io.split.engine.segments.SegmentSynchronizationTask; @@ -34,7 +35,7 @@ public SplitJmxMonitor(SplitClient splitClient, SplitFetcher featureFetcher, Spl @Override public boolean forceSyncFeatures() { - _featureFetcher.forceRefresh(true); + _featureFetcher.forceRefresh(new FetchOptions.Builder().cacheControlHeaders(true).build()); _log.info("Features successfully refreshed via JMX"); return true; } @@ -43,7 +44,7 @@ public boolean forceSyncFeatures() { public boolean forceSyncSegment(String segmentName) { SegmentFetcher fetcher = _segmentSynchronizationTask.getFetcher(segmentName); try{ - fetcher.fetch(true); + fetcher.fetch(new FetchOptions.Builder().build()); } //We are sure this will never happen because getFetcher firts initiate the segment. This try/catch is for safe only. catch (NullPointerException np){ diff --git a/client/src/main/java/io/split/client/metrics/BinarySearchLatencyTracker.java b/client/src/main/java/io/split/client/metrics/BinarySearchLatencyTracker.java deleted file mode 100644 index 35efff10f..000000000 --- a/client/src/main/java/io/split/client/metrics/BinarySearchLatencyTracker.java +++ /dev/null @@ -1,131 +0,0 @@ -package io.split.client.metrics; - -import java.util.Arrays; - -/** - * Tracks latencies pero bucket of time. - * Each bucket represent a latency greater than the one before - * and each number within each bucket is a number of calls in the range. - *

- * (1) 1.00 - * (2) 1.50 - * (3) 2.25 - * (4) 3.38 - * (5) 5.06 - * (6) 7.59 - * (7) 11.39 - * (8) 17.09 - * (9) 25.63 - * (10) 38.44 - * (11) 57.67 - * (12) 86.50 - * (13) 129.75 - * (14) 194.62 - * (15) 291.93 - * (16) 437.89 - * (17) 656.84 - * (18) 985.26 - * (19) 1,477.89 - * (20) 2,216.84 - * (21) 3,325.26 - * (22) 4,987.89 - * (23) 7,481.83 - *

- * Thread-safety: This class is not thread safe. - *

- * Created by patricioe on 2/10/16. - */ -public class BinarySearchLatencyTracker implements ILatencyTracker { - - static final long[] BUCKETS = { - 1000, 1500, 2250, 3375, 5063, - 7594, 11391, 17086, 25629, 38443, - 57665, 86498, 129746, 194620, 291929, - 437894, 656841, 985261, 1477892, 2216838, - 3325257, 4987885, 7481828 - }; - - static final long MAX_LATENCY = 7481828; - - long[] latencies = new long[BUCKETS.length]; - - /** - * Increment the internal counter for the bucket this latency falls into. - * - * @param millis - */ - public void addLatencyMillis(long millis) { - int index = findIndex(millis * 1000); - latencies[index]++; - } - - /** - * Increment the internal counter for the bucket this latency falls into. - * - * @param micros - */ - public void addLatencyMicros(long micros) { - int index = findIndex(micros); - latencies[index]++; - } - - /** - * Returns the list of latencies buckets as an array. - * - * @return the list of latencies buckets as an array. - */ - public long[] getLatencies() { - return latencies; - } - - @Override - public long getLatency(int index) { - return latencies[index]; - } - - public void clear() { - latencies = new long[BUCKETS.length]; - } - - /** - * Returns the counts in the bucket this latency falls into. - * The latencies will no be updated. - * - * @param latency - * @return the bucket content for the latency. - */ - public long getBucketForLatencyMillis(long latency) { - return latencies[findIndex(latency * 1000)]; - } - - /** - * Returns the counts in the bucket this latency falls into. - * The latencies will no be updated. - * - * @param latency - * @return the bucket content for the latency. - */ - public long getBucketForLatencyMicros(long latency) { - return latencies[findIndex(latency)]; - } - - - private int findIndex(long micros) { - if (micros > MAX_LATENCY) { - return BUCKETS.length - 1; - } - - int index = Arrays.binarySearch(BUCKETS, micros); - - if (index < 0) { - - // Adjust the index based on Java Array javadocs. <0 means the value wasn't found and it's module value - // is where it should be inserted (in this case, it means the counter it applies - unless it's equals to the - // length of the array). - - index = -(index + 1); - } - return index; - } - -} diff --git a/client/src/main/java/io/split/client/metrics/CachedMetrics.java b/client/src/main/java/io/split/client/metrics/CachedMetrics.java deleted file mode 100644 index 41f066a38..000000000 --- a/client/src/main/java/io/split/client/metrics/CachedMetrics.java +++ /dev/null @@ -1,142 +0,0 @@ - -package io.split.client.metrics; - -import com.google.common.collect.Maps; -import com.google.common.primitives.Longs; -import io.split.client.dtos.Counter; -import io.split.client.dtos.Latency; -import io.split.engine.metrics.Metrics; - -import java.util.Map; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -import static com.google.common.base.Preconditions.checkArgument; - - -/** - * Created by adilaijaz on 9/4/15. - */ -public class CachedMetrics implements Metrics { - - private final DTOMetrics _metrics; - - private final Map _latencyMap; - private final Map _countMap; - - - private final Object _latencyLock = new Object(); - private AtomicLong _latencyLastUpdateTimeMillis = new AtomicLong(System.currentTimeMillis()); - - private final Object _counterLock = new Object(); - private AtomicLong _counterLastUpdateTimeMillis = new AtomicLong(System.currentTimeMillis()); - - private long _refreshPeriodInMillis; - - private final int _queueForTheseManyCalls; - - /** - * For unit testing only. - * - * @param httpMetrics - * @param queueForTheseManyCalls - */ - /*package private*/ CachedMetrics(DTOMetrics httpMetrics, int queueForTheseManyCalls) { - this(httpMetrics, queueForTheseManyCalls, TimeUnit.MINUTES.toMillis(1)); - } - - public CachedMetrics(DTOMetrics httpMetrics, long refreshPeriodInMillis) { - this(httpMetrics, 100, refreshPeriodInMillis); - } - - private CachedMetrics(DTOMetrics metrics, int queueForTheseManyCalls, long refreshPeriodInMillis) { - _metrics = metrics; - _latencyMap = Maps.newHashMap(); - _countMap = Maps.newHashMap(); - checkArgument(queueForTheseManyCalls > 0, "queue for cache should be greater than zero"); - _queueForTheseManyCalls = queueForTheseManyCalls; - _refreshPeriodInMillis = refreshPeriodInMillis; - } - - @Override - public void count(String counter, long delta) { - if (delta <= 0) { - return; - } - - if (counter == null || counter.trim().isEmpty()) { - return; - } - - synchronized (_counterLock) { - SumAndCount sumAndCount = _countMap.get(counter); - if (sumAndCount == null) { - sumAndCount = new SumAndCount(); - _countMap.put(counter, sumAndCount); - } - - sumAndCount.addDelta(delta); - - if (sumAndCount._count >= _queueForTheseManyCalls || hasTimeElapsed(_counterLastUpdateTimeMillis)) { - Counter dto = new Counter(); - dto.name = counter; - dto.delta = sumAndCount._sum; - - sumAndCount.clear(); - _counterLastUpdateTimeMillis.set(System.currentTimeMillis()); - _metrics.count(dto); - } - } - } - - private boolean hasTimeElapsed(AtomicLong lastRefreshTime) { - return (System.currentTimeMillis() - lastRefreshTime.get()) > _refreshPeriodInMillis; - } - - @Override - public void time(String operation, long timeInMs) { - if (operation == null || operation.trim().isEmpty() || timeInMs < 0L) { - // error - return; - } - synchronized (_latencyLock) { - if (!_latencyMap.containsKey(operation)) { - ILatencyTracker latencies = new BinarySearchLatencyTracker(); - _latencyMap.put(operation, latencies); - } - - ILatencyTracker tracker = _latencyMap.get(operation); - tracker.addLatencyMillis((int) timeInMs); - - if (hasTimeElapsed(_latencyLastUpdateTimeMillis)) { - - Latency dto = new Latency(); - dto.name = operation; - dto.latencies = Longs.asList(tracker.getLatencies()); - - tracker.clear(); - _latencyLastUpdateTimeMillis.set(System.currentTimeMillis()); - _metrics.time(dto); - - } - } - } - - - private static final class SumAndCount { - private int _count = 0; - private long _sum = 0L; - - public void addDelta(long delta) { - _count++; - _sum += delta; - } - - public void clear() { - _count = 0; - _sum = 0L; - } - - } - -} diff --git a/client/src/main/java/io/split/client/metrics/DTOMetrics.java b/client/src/main/java/io/split/client/metrics/DTOMetrics.java deleted file mode 100644 index 86c793cb6..000000000 --- a/client/src/main/java/io/split/client/metrics/DTOMetrics.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.split.client.metrics; - -import io.split.client.dtos.Counter; -import io.split.client.dtos.Latency; - -/** - * Created by adilaijaz on 6/14/16. - */ -public interface DTOMetrics { - void time(Latency dto); - - void count(Counter dto); -} diff --git a/client/src/main/java/io/split/client/metrics/FireAndForgetMetrics.java b/client/src/main/java/io/split/client/metrics/FireAndForgetMetrics.java deleted file mode 100644 index 43aa702de..000000000 --- a/client/src/main/java/io/split/client/metrics/FireAndForgetMetrics.java +++ /dev/null @@ -1,123 +0,0 @@ -package io.split.client.metrics; - -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import io.split.engine.metrics.Metrics; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.Closeable; -import java.util.List; -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Created by adilaijaz on 9/4/15. - */ -public class FireAndForgetMetrics implements Metrics, Closeable { - - private static final Logger _log = LoggerFactory.getLogger(FireAndForgetMetrics.class); - - private final ExecutorService _executorService; - private final Metrics _delegate; - - public static FireAndForgetMetrics instance(Metrics delegate, int numberOfThreads, int queueSize) { - ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder(); - threadFactoryBuilder.setDaemon(true); - threadFactoryBuilder.setNameFormat("split-fireAndForgetMetrics-%d"); - threadFactoryBuilder.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread t, Throwable e) { - _log.error("Error in thread: " + t.getName(), e); - } - }); - - final ExecutorService executorService = new ThreadPoolExecutor(numberOfThreads, - numberOfThreads, - 0L, - TimeUnit.MILLISECONDS, - new ArrayBlockingQueue(queueSize), - threadFactoryBuilder.build(), - new ThreadPoolExecutor.DiscardPolicy()); - - - return new FireAndForgetMetrics(delegate, executorService); - } - - private FireAndForgetMetrics(Metrics delegate, ExecutorService executorService) { - _delegate = delegate; - _executorService = executorService; - } - - - @Override - public void count(String counter, long delta) { - try { - _executorService.submit(new CountRunnable(_delegate, counter, delta)); - } catch (Throwable t) { - _log.warn("CountRunnable failed", t); - } - } - - @Override - public void time(String operation, long timeInMs) { - try { - _executorService.submit(new TimeRunnable(_delegate, operation, timeInMs)); - } catch (Throwable t) { - _log.warn("TimeRunnable failed", t); - } - } - - public void close() { - _executorService.shutdown(); - try { - if (!_executorService.awaitTermination(10L, TimeUnit.SECONDS)) { //optional * - _log.info("Executor did not terminate in the specified time."); - List droppedTasks = _executorService.shutdownNow(); //optional ** - _log.info("Executor was abruptly shut down. These tasks will not be executed: " + droppedTasks); - } - } catch (InterruptedException e) { - // reset the interrupt. - Thread.currentThread().interrupt(); - } - } - - - private static final class CountRunnable implements Runnable { - - private final Metrics _delegate; - private final String _name; - private final long _delta; - - public CountRunnable(Metrics delegate, String name, long delta) { - _delegate = delegate; - _name = name; - _delta = delta; - } - - @Override - public void run() { - _delegate.count(_name, _delta); - } - } - - private static final class TimeRunnable implements Runnable { - - private final Metrics _delegate; - private final String _name; - private final long _timeInMs; - - public TimeRunnable(Metrics delegate, String name, long timeInMs) { - _delegate = delegate; - _name = name; - _timeInMs = timeInMs; - } - - @Override - public void run() { - _delegate.time(_name, _timeInMs); - } - } - -} diff --git a/client/src/main/java/io/split/client/metrics/HttpMetrics.java b/client/src/main/java/io/split/client/metrics/HttpMetrics.java deleted file mode 100644 index 14d63b0f4..000000000 --- a/client/src/main/java/io/split/client/metrics/HttpMetrics.java +++ /dev/null @@ -1,135 +0,0 @@ -package io.split.client.metrics; - -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.Lists; -import io.split.client.dtos.Counter; -import io.split.client.dtos.Latency; -import io.split.client.utils.Utils; -import io.split.engine.metrics.Metrics; -import org.apache.hc.client5.http.classic.methods.HttpPost; -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; -import org.apache.hc.core5.http.HttpEntity; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.net.URI; -import java.net.URISyntaxException; - -/** - * Created by adilaijaz on 9/4/15. - */ -public class HttpMetrics implements Metrics, DTOMetrics { - private static final Logger _log = LoggerFactory.getLogger(HttpMetrics.class); - - private final CloseableHttpClient _client; - private final URI _timeTarget; - private final URI _counterTarget; - - - public static HttpMetrics create(CloseableHttpClient client, URI root) throws URISyntaxException { - return new HttpMetrics(client, root); - } - - - public HttpMetrics(CloseableHttpClient client, URI root) throws URISyntaxException { - Preconditions.checkNotNull(root); - _client = Preconditions.checkNotNull(client); - _timeTarget = Utils.appendPath(root, "api/metrics/time"); - _counterTarget = Utils.appendPath(root, "api/metrics/counter"); - } - - - @Override - public void time(Latency dto) { - if (dto.latencies.isEmpty()) { - return; - } - - try { - post(_timeTarget, dto); - } catch (Throwable t) { - _log.warn("Exception when posting metric " + dto, t); - } - ; - - } - - @Override - public void count(Counter dto) { - try { - post(_counterTarget, dto); - } catch (Throwable t) { - _log.warn("Exception when posting metric " + dto, t); - } - - } - - private void post(URI uri, Object dto) { - - CloseableHttpResponse response = null; - - try { - HttpEntity entity = Utils.toJsonEntity(dto); - - HttpPost request = new HttpPost(uri); - request.setEntity(entity); - - response = _client.execute(request); - - int status = response.getCode(); - - if (status < 200 || status >= 300) { - _log.warn("Response status was: " + status); - } - - } catch (Throwable t) { - _log.warn("Exception when posting metrics: " + t.getMessage()); - if (_log.isDebugEnabled()) { - _log.debug("Reason: ", t); - } - } finally { - Utils.forceClose(response); - } - - } - - @Override - public void count(String counter, long delta) { - try { - Counter dto = new Counter(); - dto.name = counter; - dto.delta = delta; - - count(dto); - } catch (Throwable t) { - _log.info("Could not count metric " + counter, t); - } - - } - - @Override - public void time(String operation, long timeInMs) { - try { - Latency dto = new Latency(); - dto.name = operation; - dto.latencies = Lists.newArrayList(timeInMs); - - time(dto); - } catch (Throwable t) { - _log.info("Could not time metric " + operation, t); - } - } - - @VisibleForTesting - URI getTimeTarget() { - return _timeTarget; - } - - @VisibleForTesting - URI getCounterTarget() { - return _counterTarget; - } - -} diff --git a/client/src/main/java/io/split/client/metrics/ILatencyTracker.java b/client/src/main/java/io/split/client/metrics/ILatencyTracker.java deleted file mode 100644 index 282db06cb..000000000 --- a/client/src/main/java/io/split/client/metrics/ILatencyTracker.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.split.client.metrics; - -/** - * Created by patricioe on 2/10/16. - */ -public interface ILatencyTracker { - - void addLatencyMillis(long millis); - - void addLatencyMicros(long micros); - - long[] getLatencies(); - - long getLatency(int index); - - void clear(); - - long getBucketForLatencyMillis(long latency); - - long getBucketForLatencyMicros(long latency); - -} diff --git a/client/src/main/java/io/split/client/metrics/LogarithmicSearchLatencyTracker.java b/client/src/main/java/io/split/client/metrics/LogarithmicSearchLatencyTracker.java deleted file mode 100644 index 4034d8de6..000000000 --- a/client/src/main/java/io/split/client/metrics/LogarithmicSearchLatencyTracker.java +++ /dev/null @@ -1,116 +0,0 @@ -package io.split.client.metrics; - -/** - * Tracks latencies pero bucket of time. - * Each bucket represent a latency greater than the one before - * and each number within each bucket is a number of calls in the range. - *

- * (1) 1.00 - * (2) 1.50 - * (3) 2.25 - * (4) 3.38 - * (5) 5.06 - * (6) 7.59 - * (7) 11.39 - * (8) 17.09 - * (9) 25.63 - * (10) 38.44 - * (11) 57.67 - * (12) 86.50 - * (13) 129.75 - * (14) 194.62 - * (15) 291.93 - * (16) 437.89 - * (17) 656.84 - * (18) 985.26 - * (19) 1,477.89 - * (20) 2,216.84 - * (21) 3,325.26 - * (22) 4,987.89 - * (23) 7,481.83 - *

- * Thread-safety: This class is not thread safe. - *

- * Created by patricioe on 2/10/16. - */ -public class LogarithmicSearchLatencyTracker implements ILatencyTracker { - - static final int BUCKETS = 23; - private static final double LOG_10_1000_MICROS = Math.log10(1000); - private static final double LOG_10_1_5_MICROS = Math.log10(Double.valueOf("1.5").doubleValue()); - - - long[] latencies = new long[BUCKETS]; - - /** - * Increment the internal counter for the bucket this latency falls into. - * - * @param millis - */ - public void addLatencyMillis(long millis) { - int index = findIndex(millis * 1000); - latencies[index]++; - } - - /** - * Increment the internal counter for the bucket this latency falls into. - * - * @param micros - */ - public void addLatencyMicros(long micros) { - int index = findIndex(micros); - latencies[index]++; - } - - /** - * Returns the list of latencies buckets as an array. - * - * @return the list of latencies buckets as an array. - */ - public long[] getLatencies() { - return latencies; - } - - @Override - public long getLatency(int index) { - return latencies[index]; - } - - public void clear() { - latencies = new long[BUCKETS]; - } - - /** - * Returns the counts in the bucket this latency falls into. - * The latencies will no be updated. - * - * @param latency - * @return the bucket content for the latency. - */ - public long getBucketForLatencyMillis(long latency) { - return latencies[findIndex(latency * 1000)]; - } - - /** - * Returns the counts in the bucket this latency falls into. - * The latencies will no be updated. - * - * @param latency - * @return the bucket content for the latency. - */ - public long getBucketForLatencyMicros(long latency) { - return latencies[findIndex(latency)]; - } - - - private int findIndex(long micros) { - - if (micros <= 1000) return 0; - if (micros > 4987885) return 22; - - double raw = (Math.log10(micros) - LOG_10_1000_MICROS) / LOG_10_1_5_MICROS; - double rounded = Math.round(raw * 1000000d) / 1000000d; - return (int) Math.ceil(rounded); - } - -} diff --git a/client/src/main/java/io/split/engine/SDKReadinessGates.java b/client/src/main/java/io/split/engine/SDKReadinessGates.java index ae0fd8ad4..10a18fbba 100644 --- a/client/src/main/java/io/split/engine/SDKReadinessGates.java +++ b/client/src/main/java/io/split/engine/SDKReadinessGates.java @@ -15,9 +15,7 @@ public class SDKReadinessGates { private static final Logger _log = LoggerFactory.getLogger(SDKReadinessGates.class); - private final CountDownLatch _splitsAreReady = new CountDownLatch(1); - private final ConcurrentMap _segmentsAreReady = new ConcurrentHashMap<>(); - + private final CountDownLatch _internalReady = new CountDownLatch(1); /** * Returns true if the SDK is ready. The SDK is ready when: @@ -34,136 +32,15 @@ public class SDKReadinessGates { * @return true if the sdk is ready, false otherwise. * @throws InterruptedException if this operation was interrupted. */ - public boolean isSDKReady(long milliseconds) throws InterruptedException { - long end = System.currentTimeMillis() + milliseconds; - long timeLeft = milliseconds; - - boolean splits = areSplitsReady(timeLeft); - if (!splits) { - return false; - } - - timeLeft = end - System.currentTimeMillis(); - - return areSegmentsReady(timeLeft); - } - - public boolean isSDKReadyNow() { - try { - return isSDKReady(0); - } catch (InterruptedException e) { - return false; - } - } - - /** - * Records that the SDK split initialization is done. - * This operation is atomic and idempotent. Repeated invocations - * will not have any impact on the state. - */ - public void splitsAreReady() { - long originalCount = _splitsAreReady.getCount(); - _splitsAreReady.countDown(); - if (originalCount > 0L) { - _log.info("splits are ready"); - } - } - - /** - * Registers a segment that the SDK should download before it is ready. - * This method should be called right after the first successful download - * of split definitions. - *

- * Note that if this method is called in subsequent fetches of splits, - * it will return false; meaning any segments used in new splits - * will not be able to block the SDK from being marked as complete. - * - * @param segmentName the segment to register - * @return true if the segments were registered, false otherwise. - * @throws InterruptedException - */ - public boolean registerSegment(String segmentName) throws InterruptedException { - if (segmentName == null || segmentName.isEmpty() || areSplitsReady(0L)) { - return false; - } - - _segmentsAreReady.putIfAbsent(segmentName, new CountDownLatch(1)); - _log.info("Registered segment: " + segmentName); - return true; - } - - /** - * Records that the SDK segment initialization for this segment is done. - * This operation is atomic and idempotent. Repeated invocations - * will not have any impact on the state. - */ - public void segmentIsReady(String segmentName) { - CountDownLatch cdl = _segmentsAreReady.get(segmentName); - if (cdl == null) { - return; - } - - long originalCount = cdl.getCount(); - - cdl.countDown(); - - if (originalCount > 0L) { - _log.info(segmentName + " segment is ready"); - } - } - - public boolean isSegmentRegistered(String segmentName) { - return _segmentsAreReady.get(segmentName) != null; + public boolean waitUntilInternalReady(long milliseconds) throws InterruptedException { + return _internalReady.await(milliseconds, TimeUnit.MILLISECONDS); } - /** - * Returns true if the SDK is ready w.r.t segments. In other words, this method returns true if: - *

    - *
  1. The SDK has fetched segment definitions the first time.
  2. - *
- *

- * This operation will block until the SDK is ready or 'milliseconds' have passed. If the milliseconds - * are less than or equal to zero, the operation will not block and return immediately - * - * @param milliseconds time to wait for an answer. if the value is zero or negative, we will not - * block for an answer. - * @return true if the sdk is ready w.r.t splits, false otherwise. - * @throws InterruptedException if this operation was interrupted. - */ - public boolean areSegmentsReady(long milliseconds) throws InterruptedException { - long end = System.currentTimeMillis() + milliseconds; - long timeLeft = milliseconds; - - for (Map.Entry entry : _segmentsAreReady.entrySet()) { - String segmentName = entry.getKey(); - CountDownLatch cdl = entry.getValue(); - - if (!cdl.await(timeLeft, TimeUnit.MILLISECONDS)) { - _log.error(segmentName + " is not ready yet"); - return false; - } - - timeLeft = end - System.currentTimeMillis(); - } - - return true; + public boolean isSDKReady() { + return _internalReady.getCount() == 0; } - /** - * Returns true if the SDK is ready w.r.t splits. In other words, this method returns true if: - *

    - *
  1. The SDK has fetched Split definitions the first time.
  2. - *
- *

- * This operation will block until the SDK is ready or 'milliseconds' have passed. If the milliseconds - * are less than or equal to zero, the operation will not block and return immediately - * - * @param milliseconds time to wait for an answer. if the value is zero or negative, we will not - * block for an answer. - * @return true if the sdk is ready w.r.t splits, false otherwise. - * @throws InterruptedException if this operation was interrupted. - */ - public boolean areSplitsReady(long milliseconds) throws InterruptedException { - return _splitsAreReady.await(milliseconds, TimeUnit.MILLISECONDS); + public void sdkInternalReady() { + _internalReady.countDown(); } } diff --git a/client/src/main/java/io/split/engine/common/Backoff.java b/client/src/main/java/io/split/engine/common/Backoff.java index 13bcb5425..285e0c824 100644 --- a/client/src/main/java/io/split/engine/common/Backoff.java +++ b/client/src/main/java/io/split/engine/common/Backoff.java @@ -5,20 +5,26 @@ import static com.google.common.base.Preconditions.checkNotNull; public class Backoff { - private static final long BACKOFF_MAX_SECONDS_ALLOWED = 1800; + private static final long BACKOFF_MAX_ALLOWED = 1800; private final long _backoffBase; private AtomicInteger _attempt; + private final long _maxAllowed; public Backoff(long backoffBase) { + this(backoffBase, BACKOFF_MAX_ALLOWED); + } + + public Backoff(long backoffBase, long maxAllowed) { _backoffBase = checkNotNull(backoffBase); _attempt = new AtomicInteger(0); + _maxAllowed = maxAllowed; } public long interval() { long interval = _backoffBase * (long) Math.pow(2, _attempt.getAndIncrement()); - return interval >= BACKOFF_MAX_SECONDS_ALLOWED ? BACKOFF_MAX_SECONDS_ALLOWED : interval; + return interval >= _maxAllowed ? BACKOFF_MAX_ALLOWED : interval; } public synchronized void reset() { diff --git a/client/src/main/java/io/split/engine/common/FastlyHeadersCaptor.java b/client/src/main/java/io/split/engine/common/FastlyHeadersCaptor.java new file mode 100644 index 000000000..143f9523b --- /dev/null +++ b/client/src/main/java/io/split/engine/common/FastlyHeadersCaptor.java @@ -0,0 +1,35 @@ +package io.split.engine.common; + +import java.util.*; +import java.util.stream.Collectors; + +public class FastlyHeadersCaptor { + + private static final Set HEADERS_TO_CAPTURE = new HashSet<>(Arrays.asList( + "Fastly-Debug-Path", + "Fastly-Debug-TTL", + "Fastly-Debug-Digest", + "X-Served-By", + "X-Cache", + "X-Cache-Hits", + "X-Timer", + "Surrogate-Key", + "ETag", + "Cache-Control", + "X-Request-ID", + "Last-Modified" + )); + + private final List> _headers = new ArrayList<>(); + + public Void handle(Map responseHeaders) { + _headers.add(responseHeaders.entrySet().stream() + .filter(e -> HEADERS_TO_CAPTURE.contains(e.getKey())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))); + return null; + } + + public List> get() { + return _headers; + } +} \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/common/FetchOptions.java b/client/src/main/java/io/split/engine/common/FetchOptions.java new file mode 100644 index 000000000..e1996b3e8 --- /dev/null +++ b/client/src/main/java/io/split/engine/common/FetchOptions.java @@ -0,0 +1,104 @@ +package io.split.engine.common; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Function; + +public class FetchOptions { + + public static final Long DEFAULT_TARGET_CHANGENUMBER = -1L; + + public static class Builder { + + public Builder() {} + + public Builder(FetchOptions opts) { + _targetCN = opts._targetCN; + _cacheControlHeaders = opts._cacheControlHeaders; + _fastlyDebugHeader = opts._fastlyDebugHeader; + _responseHeadersCallback = opts._responseHeadersCallback; + } + + public Builder cacheControlHeaders(boolean on) { + _cacheControlHeaders = on; + return this; + } + + public Builder fastlyDebugHeader(boolean on) { + _fastlyDebugHeader = on; + return this; + } + + public Builder responseHeadersCallback(Function, Void> callback) { + _responseHeadersCallback = callback; + return this; + } + + public Builder targetChangeNumber(long targetCN) { + _targetCN = targetCN; + return this; + } + + public FetchOptions build() { + return new FetchOptions(_cacheControlHeaders, _targetCN, _responseHeadersCallback, _fastlyDebugHeader); + } + + private long _targetCN = DEFAULT_TARGET_CHANGENUMBER; + private boolean _cacheControlHeaders = false; + private boolean _fastlyDebugHeader = false; + private Function, Void> _responseHeadersCallback = null; + } + + public boolean cacheControlHeadersEnabled() { + return _cacheControlHeaders; + } + + public boolean fastlyDebugHeaderEnabled() { + return _fastlyDebugHeader; + } + + public long targetCN() { return _targetCN; } + + public boolean hasCustomCN() { return _targetCN != DEFAULT_TARGET_CHANGENUMBER; } + + public void handleResponseHeaders(Map headers) { + if (Objects.isNull(_responseHeadersCallback) || Objects.isNull(headers)) { + return; + } + _responseHeadersCallback.apply(headers); + } + + private FetchOptions(boolean cacheControlHeaders, + long targetCN, + Function, Void> responseHeadersCallback, + boolean fastlyDebugHeader) { + _cacheControlHeaders = cacheControlHeaders; + _targetCN = targetCN; + _responseHeadersCallback = responseHeadersCallback; + _fastlyDebugHeader = fastlyDebugHeader; + } + + @Override + public boolean equals(Object obj) { + if (null == obj) return false; + if (this == obj) return true; + if (!(obj instanceof FetchOptions)) return false; + + FetchOptions other = (FetchOptions) obj; + + return Objects.equals(_cacheControlHeaders, other._cacheControlHeaders) + && Objects.equals(_fastlyDebugHeader, other._fastlyDebugHeader) + && Objects.equals(_responseHeadersCallback, other._responseHeadersCallback) + && Objects.equals(_targetCN, other._targetCN); + } + + @Override + public int hashCode() { + return com.google.common.base.Objects.hashCode(_cacheControlHeaders, _fastlyDebugHeader, _responseHeadersCallback, _targetCN); + } + + private final boolean _cacheControlHeaders; + private final boolean _fastlyDebugHeader; + private final long _targetCN; + private final Function, Void> _responseHeadersCallback; +} 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 1d770b6b1..9585bc1a7 100644 --- a/client/src/main/java/io/split/engine/common/PushManagerImp.java +++ b/client/src/main/java/io/split/engine/common/PushManagerImp.java @@ -16,6 +16,9 @@ import io.split.engine.sse.workers.SplitsWorkerImp; import io.split.engine.sse.workers.Worker; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.enums.StreamEventsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,13 +44,15 @@ public class PushManagerImp implements PushManager { private Future _nextTokenRefreshTask; private final ScheduledExecutorService _scheduledExecutorService; private AtomicLong _expirationTime; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; @VisibleForTesting /* package private */ PushManagerImp(AuthApiClient authApiClient, EventSourceClient eventSourceClient, SplitsWorker splitsWorker, Worker segmentWorker, - PushStatusTracker pushStatusTracker) { + PushStatusTracker pushStatusTracker, + TelemetryRuntimeProducer telemetryRuntimeProducer) { _authApiClient = checkNotNull(authApiClient); _eventSourceClient = checkNotNull(eventSourceClient); @@ -59,6 +64,7 @@ public class PushManagerImp implements PushManager { .setDaemon(true) .setNameFormat("Split-SSERefreshToken-%d") .build()); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } public static PushManagerImp build(Synchronizer synchronizer, @@ -66,15 +72,16 @@ public static PushManagerImp build(Synchronizer synchronizer, String authUrl, CloseableHttpClient httpClient, LinkedBlockingQueue statusMessages, - CloseableHttpClient sseHttpClient) { + CloseableHttpClient sseHttpClient, + TelemetryRuntimeProducer telemetryRuntimeProducer) { SplitsWorker splitsWorker = new SplitsWorkerImp(synchronizer); Worker segmentWorker = new SegmentsWorkerImp(synchronizer); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(statusMessages); - return new PushManagerImp(new AuthApiClientImp(authUrl, httpClient), - EventSourceClientImp.build(streamingUrl, splitsWorker, segmentWorker, pushStatusTracker, sseHttpClient), + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(statusMessages, telemetryRuntimeProducer); + return new PushManagerImp(new AuthApiClientImp(authUrl, httpClient, telemetryRuntimeProducer), + EventSourceClientImp.build(streamingUrl, splitsWorker, segmentWorker, pushStatusTracker, sseHttpClient, telemetryRuntimeProducer), splitsWorker, segmentWorker, - pushStatusTracker); + pushStatusTracker, telemetryRuntimeProducer); } @Override @@ -83,6 +90,7 @@ public synchronized void start() { _log.debug(String.format("Auth service response pushEnabled: %s", response.isPushEnabled())); if (response.isPushEnabled() && startSse(response.getToken(), response.getChannels())) { _expirationTime.set(response.getExpiration()); + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.TOKEN_REFRESH.getType(), response.getExpiration(), System.currentTimeMillis())); return; } diff --git a/client/src/main/java/io/split/engine/common/SyncManager.java b/client/src/main/java/io/split/engine/common/SyncManager.java index b6a3c9b3e..147641748 100644 --- a/client/src/main/java/io/split/engine/common/SyncManager.java +++ b/client/src/main/java/io/split/engine/common/SyncManager.java @@ -1,8 +1,5 @@ package io.split.engine.common; -import io.split.engine.sse.listeners.FeedbackLoopListener; -import io.split.engine.sse.listeners.NotificationKeeperListener; - public interface SyncManager { void start(); void shutdown(); 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 2d0fe2fa9..024ac02d2 100644 --- a/client/src/main/java/io/split/engine/common/SyncManagerImp.java +++ b/client/src/main/java/io/split/engine/common/SyncManagerImp.java @@ -4,13 +4,21 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.split.cache.SegmentCache; import io.split.cache.SplitCache; +import io.split.client.ApiKeyCounter; +import io.split.client.SplitClientConfig; +import io.split.engine.SDKReadinessGates; import io.split.engine.experiments.SplitFetcher; import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.segments.SegmentSynchronizationTaskImp; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.enums.StreamEventsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.telemetry.synchronizer.TelemetrySynchronizer; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.ArrayList; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; @@ -28,15 +36,23 @@ public class SyncManagerImp implements SyncManager { private final AtomicBoolean _shutdown; private final LinkedBlockingQueue _incomingPushStatus; private final ExecutorService _executorService; + private final ExecutorService _startExecutorService; + private final SDKReadinessGates _gates; private Future _pushStatusMonitorTask; private Backoff _backoff; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + private final TelemetrySynchronizer _telemetrySynchronizer; + private final SplitClientConfig _config; @VisibleForTesting /* package private */ SyncManagerImp(boolean streamingEnabledConfig, Synchronizer synchronizer, PushManager pushManager, LinkedBlockingQueue pushMessages, - int authRetryBackOffBase) { + int authRetryBackOffBase, + SDKReadinessGates gates, TelemetryRuntimeProducer telemetryRuntimeProducer, + TelemetrySynchronizer telemetrySynchronizer, + SplitClientConfig config) { _streamingEnabledConfig = new AtomicBoolean(streamingEnabledConfig); _synchronizer = checkNotNull(synchronizer); _pushManager = checkNotNull(pushManager); @@ -46,7 +62,15 @@ public class SyncManagerImp implements SyncManager { .setNameFormat("SPLIT-PushStatusMonitor-%d") .setDaemon(true) .build()); + _startExecutorService = Executors.newSingleThreadExecutor(new ThreadFactoryBuilder() + .setNameFormat("SPLIT-PollingMode-%d") + .setDaemon(true) + .build()); _backoff = new Backoff(authRetryBackOffBase); + _gates = checkNotNull(gates); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + _telemetrySynchronizer = checkNotNull(telemetrySynchronizer); + _config = checkNotNull(config); } public static SyncManagerImp build(boolean streamingEnabledConfig, @@ -59,20 +83,65 @@ public static SyncManagerImp build(boolean streamingEnabledConfig, String streamingServiceUrl, int authRetryBackOffBase, CloseableHttpClient sseHttpClient, - SegmentCache segmentCache) { + SegmentCache segmentCache, + int streamingRetryDelay, + int maxOnDemandFetchRetries, + int failedAttemptsBeforeLogging, + boolean cdnDebugLogging, + SDKReadinessGates gates, + TelemetryRuntimeProducer telemetryRuntimeProducer, + TelemetrySynchronizer telemetrySynchronizer, + SplitClientConfig config) { LinkedBlockingQueue pushMessages = new LinkedBlockingQueue<>(); - Synchronizer synchronizer = new SynchronizerImp(splitSynchronizationTask, splitFetcher, segmentSynchronizationTaskImp, splitCache, segmentCache); - PushManager pushManager = PushManagerImp.build(synchronizer, streamingServiceUrl, authUrl, httpClient, pushMessages, sseHttpClient); - return new SyncManagerImp(streamingEnabledConfig, synchronizer, pushManager, pushMessages, authRetryBackOffBase); + Synchronizer synchronizer = new SynchronizerImp(splitSynchronizationTask, + splitFetcher, + segmentSynchronizationTaskImp, + splitCache, + segmentCache, + streamingRetryDelay, + maxOnDemandFetchRetries, + failedAttemptsBeforeLogging, + cdnDebugLogging, + gates); + + PushManager pushManager = PushManagerImp.build(synchronizer, + streamingServiceUrl, + authUrl, + httpClient, + pushMessages, + sseHttpClient, + telemetryRuntimeProducer); + + return new SyncManagerImp(streamingEnabledConfig, + synchronizer, + pushManager, + pushMessages, + authRetryBackOffBase, + gates, + telemetryRuntimeProducer, + telemetrySynchronizer, + config); } @Override public void start() { - if (_streamingEnabledConfig.get()) { - startStreamingMode(); - } else { - startPollingMode(); - } + _startExecutorService.submit(() -> { + while(!_synchronizer.syncAll()) { + try { + Thread.currentThread().sleep(1000); + } catch (InterruptedException e) { + _log.warn("Sdk Initializer thread interrupted"); + Thread.currentThread().interrupt(); + } + } + _gates.sdkInternalReady(); + _telemetrySynchronizer.synchronizeConfig(_config, System.currentTimeMillis(), ApiKeyCounter.getApiKeyCounterInstance().getFactoryInstances(), new ArrayList<>()); + if (_streamingEnabledConfig.get()) { + startStreamingMode(); + } else { + startPollingMode(); + } + }); } @Override @@ -84,17 +153,17 @@ public void shutdown() { private void startStreamingMode() { _log.debug("Starting in streaming mode ..."); - _synchronizer.syncAll(); if (null == _pushStatusMonitorTask) { _pushStatusMonitorTask = _executorService.submit(this::incomingPushStatusHandler); } _pushManager.start(); - + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SYNC_MODE_UPDATE.getType(), StreamEventsEnum.SyncModeUpdateValues.STREAMING_EVENT.getValue(), System.currentTimeMillis())); } private void startPollingMode() { _log.debug("Starting in polling mode ..."); _synchronizer.startPeriodicFetching(); + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SYNC_MODE_UPDATE.getType(), StreamEventsEnum.SyncModeUpdateValues.POLLING_EVENT.getValue(), System.currentTimeMillis())); } @VisibleForTesting @@ -110,14 +179,17 @@ private void startPollingMode() { _pushManager.startWorkers(); _pushManager.scheduleConnectionReset(); _backoff.reset(); + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.STREAMING_STATUS.getType(), StreamEventsEnum.StreamingStatusValues.STREAMING_ENABLED.getValue(), System.currentTimeMillis())); + _log.info("Streaming up and running."); break; case STREAMING_DOWN: + _log.info("Streaming service temporarily unavailable, working in polling mode."); _pushManager.stopWorkers(); _synchronizer.startPeriodicFetching(); break; case STREAMING_BACKOFF: long howLong = _backoff.interval() * 1000; - _log.error(String.format("Retryable error in streaming subsystem. Switching to polling and retrying in %d seconds", howLong/1000)); + _log.info(String.format("Retryable error in streaming subsystem. Switching to polling and retrying in %d seconds", howLong/1000)); _synchronizer.startPeriodicFetching(); _pushManager.stopWorkers(); _pushManager.stop(); @@ -126,6 +198,7 @@ private void startPollingMode() { _pushManager.start(); break; case STREAMING_OFF: + _log.info("Unrecoverable error in streaming subsystem. SDK will work in polling-mode and will not retry an SSE connection."); _pushManager.stop(); _synchronizer.startPeriodicFetching(); if (null != _pushStatusMonitorTask) { 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 ab8467a5c..9197baacf 100644 --- a/client/src/main/java/io/split/engine/common/Synchronizer.java +++ b/client/src/main/java/io/split/engine/common/Synchronizer.java @@ -1,7 +1,7 @@ package io.split.engine.common; public interface Synchronizer { - void syncAll(); + boolean syncAll(); void startPeriodicFetching(); void stopPeriodicFetching(); void refreshSplits(long targetChangeNumber); 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 0d22ff9f2..9214d284b 100644 --- a/client/src/main/java/io/split/engine/common/SynchronizerImp.java +++ b/client/src/main/java/io/split/engine/common/SynchronizerImp.java @@ -1,8 +1,11 @@ package io.split.engine.common; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import io.split.cache.SegmentCache; import io.split.cache.SplitCache; +import io.split.engine.SDKReadinessGates; import io.split.engine.experiments.SplitFetcher; import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.segments.SegmentFetcher; @@ -10,48 +13,59 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.TimeUnit; +import java.util.List; +import java.util.Map; +import java.util.concurrent.*; +import java.util.function.Function; import static com.google.common.base.Preconditions.checkNotNull; public class SynchronizerImp implements Synchronizer { - private static final Logger _log = LoggerFactory.getLogger(Synchronizer.class); - private static final int RETRIES_NUMBER = 10; + // The boxing here IS necessary, so that the constants are not inlined by the compiler + // and can be modified for the test (we don't want to wait that much in an UT) + private static final long ON_DEMAND_FETCH_BACKOFF_BASE_MS = new Long(10000); //backoff base starting at 10 seconds (!) + private static final long ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_MS = new Long(60000); // don't sleep for more than 1 second + private static final int ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES = 10; + + private static final Logger _log = LoggerFactory.getLogger(Synchronizer.class); private final SplitSynchronizationTask _splitSynchronizationTask; private final SplitFetcher _splitFetcher; private final SegmentSynchronizationTask _segmentSynchronizationTaskImp; - private final ScheduledExecutorService _syncAllScheduledExecutorService; private final SplitCache _splitCache; private final SegmentCache _segmentCache; + private final int _onDemandFetchRetryDelayMs; + private final int _onDemandFetchMaxRetries; + private final int _failedAttemptsBeforeLogging; + private final boolean _cdnResponseHeadersLogging; + + private final Gson gson = new GsonBuilder().create(); public SynchronizerImp(SplitSynchronizationTask splitSynchronizationTask, SplitFetcher splitFetcher, SegmentSynchronizationTask segmentSynchronizationTaskImp, SplitCache splitCache, - SegmentCache segmentCache) { + SegmentCache segmentCache, + int onDemandFetchRetryDelayMs, + int onDemandFetchMaxRetries, + int failedAttemptsBeforeLogging, + boolean cdnResponseHeadersLogging, + SDKReadinessGates gates) { _splitSynchronizationTask = checkNotNull(splitSynchronizationTask); _splitFetcher = checkNotNull(splitFetcher); _segmentSynchronizationTaskImp = checkNotNull(segmentSynchronizationTaskImp); _splitCache = checkNotNull(splitCache); _segmentCache = checkNotNull(segmentCache); + _onDemandFetchRetryDelayMs = checkNotNull(onDemandFetchRetryDelayMs); + _cdnResponseHeadersLogging = cdnResponseHeadersLogging; + _onDemandFetchMaxRetries = onDemandFetchMaxRetries; + _failedAttemptsBeforeLogging = failedAttemptsBeforeLogging; - ThreadFactory splitsThreadFactory = new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("Split-SyncAll-%d") - .build(); - _syncAllScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(splitsThreadFactory); } @Override - public void syncAll() { - _syncAllScheduledExecutorService.schedule(() -> { - _splitFetcher.fetchAll(true); - _segmentSynchronizationTaskImp.fetchAll(true); - }, 0, TimeUnit.SECONDS); + public boolean syncAll() { + return _splitFetcher.fetchAll(new FetchOptions.Builder().cacheControlHeaders(true).build()) && _segmentSynchronizationTaskImp.fetchAllSynchronous(); } @Override @@ -68,12 +82,91 @@ public void stopPeriodicFetching() { _segmentSynchronizationTaskImp.stop(); } + private static class SyncResult { + + /* package private */ SyncResult(boolean success, int remainingAttempts) { + _success = success; + _remainingAttempts = remainingAttempts; + } + + public boolean success() { return _success; } + public int remainingAttempts() { return _remainingAttempts; } + + private final boolean _success; + private final int _remainingAttempts; + } + + private SyncResult attemptSplitsSync(long targetChangeNumber, + FetchOptions opts, + Function nextWaitMs, + int maxRetries) { + int remainingAttempts = maxRetries; + while(true) { + remainingAttempts--; + _splitFetcher.forceRefresh(opts); + if (targetChangeNumber <= _splitCache.getChangeNumber()) { + return new SyncResult(true, remainingAttempts); + } else if (remainingAttempts <= 0) { + return new SyncResult(false, remainingAttempts); + } + try { + long howLong = nextWaitMs.apply(null); + Thread.sleep(howLong); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + _log.debug("Error trying to sleep current Thread."); + } + } + } + + private void logCdnHeaders(String prefix, int maxRetries, int remainingAttempts, List> headers) { + if (maxRetries - remainingAttempts > _failedAttemptsBeforeLogging) { + _log.info(String.format("%s: CDN Debug headers: %s", prefix, gson.toJson(headers))); + } + } + @Override public void refreshSplits(long targetChangeNumber) { - int retries = 1; - while(targetChangeNumber > _splitCache.getChangeNumber() && retries <= RETRIES_NUMBER) { - _splitFetcher.forceRefresh(true); - retries++; + + if (targetChangeNumber <= _splitCache.getChangeNumber()) { + return; + } + + FastlyHeadersCaptor captor = new FastlyHeadersCaptor(); + FetchOptions opts = new FetchOptions.Builder() + .cacheControlHeaders(true) + .fastlyDebugHeader(_cdnResponseHeadersLogging) + .responseHeadersCallback(_cdnResponseHeadersLogging ? captor::handle : null) + .build(); + + SyncResult regularResult = attemptSplitsSync(targetChangeNumber, opts, + (discard) -> (long) _onDemandFetchRetryDelayMs, _onDemandFetchMaxRetries); + + int attempts = _onDemandFetchMaxRetries - regularResult.remainingAttempts(); + if (regularResult.success()) { + _log.debug(String.format("Refresh completed in %s attempts.", attempts)); + if (_cdnResponseHeadersLogging) { + logCdnHeaders("[splits]", _onDemandFetchMaxRetries , regularResult.remainingAttempts(), captor.get()); + } + return; + } + + _log.info(String.format("No changes fetched after %s attempts. Will retry bypassing CDN.", attempts)); + FetchOptions withCdnBypass = new FetchOptions.Builder(opts).targetChangeNumber(targetChangeNumber).build(); + Backoff backoff = new Backoff(ON_DEMAND_FETCH_BACKOFF_BASE_MS, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_MS); + SyncResult withCDNBypassed = attemptSplitsSync(targetChangeNumber, withCdnBypass, + (discard) -> backoff.interval(), ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + int withoutCDNAttempts = ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - withCDNBypassed._remainingAttempts; + if (withCDNBypassed.success()) { + _log.debug(String.format("Refresh completed bypassing the CDN in %s attempts.", withoutCDNAttempts)); + } else { + _log.debug(String.format("No changes fetched after %s attempts with CDN bypassed.", withoutCDNAttempts)); + } + + if (_cdnResponseHeadersLogging) { + logCdnHeaders("[splits]", _onDemandFetchMaxRetries + ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES, + withCDNBypassed.remainingAttempts(), captor.get()); } } @@ -85,19 +178,76 @@ public void localKillSplit(String splitName, String defaultTreatment, long newCh } } - @Override - public void refreshSegment(String segmentName, long changeNumber) { - int retries = 1; - while(changeNumber > _segmentCache.getChangeNumber(segmentName) && retries <= RETRIES_NUMBER) { - SegmentFetcher fetcher = _segmentSynchronizationTaskImp.getFetcher(segmentName); - try{ - fetcher.fetch(true); + public SyncResult attemptSegmentSync(String segmentName, + long targetChangeNumber, + FetchOptions opts, + Function nextWaitMs, + int maxRetries) { + + int remainingAttempts = maxRetries; + SegmentFetcher fetcher = _segmentSynchronizationTaskImp.getFetcher(segmentName); + checkNotNull(fetcher); + + while(true) { + remainingAttempts--; + fetcher.fetch(opts); + if (targetChangeNumber <= _segmentCache.getChangeNumber(segmentName)) { + return new SyncResult(true, remainingAttempts); + } else if (remainingAttempts <= 0) { + return new SyncResult(false, remainingAttempts); } - //We are sure this will never happen because getFetcher firts initiate the segment. This try/catch is for safe only. - catch (NullPointerException np){ - throw new NullPointerException(); + try { + long howLong = nextWaitMs.apply(null); + Thread.sleep(howLong); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + _log.debug("Error trying to sleep current Thread."); } - retries++; + } + } + + @Override + public void refreshSegment(String segmentName, long targetChangeNumber) { + + if (targetChangeNumber <= _segmentCache.getChangeNumber(segmentName)) { + return; + } + + FastlyHeadersCaptor captor = new FastlyHeadersCaptor(); + FetchOptions opts = new FetchOptions.Builder() + .cacheControlHeaders(true) + .fastlyDebugHeader(_cdnResponseHeadersLogging) + .responseHeadersCallback(_cdnResponseHeadersLogging ? captor::handle : null) + .build(); + + SyncResult regularResult = attemptSegmentSync(segmentName, targetChangeNumber, opts, + (discard) -> (long) _onDemandFetchRetryDelayMs, _onDemandFetchMaxRetries); + + int attempts = _onDemandFetchMaxRetries - regularResult.remainingAttempts(); + if (regularResult.success()) { + _log.debug(String.format("Segment %s refresh completed in %s attempts.", segmentName, attempts)); + if (_cdnResponseHeadersLogging) { + logCdnHeaders(String.format("[segment/%s]", segmentName), _onDemandFetchMaxRetries , regularResult.remainingAttempts(), captor.get()); + } + return; + } + + _log.info(String.format("No changes fetched for segment %s after %s attempts. Will retry bypassing CDN.", segmentName, attempts)); + FetchOptions withCdnBypass = new FetchOptions.Builder(opts).targetChangeNumber(targetChangeNumber).build(); + Backoff backoff = new Backoff(ON_DEMAND_FETCH_BACKOFF_BASE_MS, ON_DEMAND_FETCH_BACKOFF_MAX_WAIT_MS); + SyncResult withCDNBypassed = attemptSegmentSync(segmentName, targetChangeNumber, withCdnBypass, + (discard) -> backoff.interval(), ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES); + + int withoutCDNAttempts = ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES - withCDNBypassed._remainingAttempts; + if (withCDNBypassed.success()) { + _log.debug(String.format("Segment %s refresh completed bypassing the CDN in %s attempts.", segmentName, withoutCDNAttempts)); + } else { + _log.debug(String.format("No changes fetched for segment %s after %s attempts with CDN bypassed.", segmentName, withoutCDNAttempts)); + } + + if (_cdnResponseHeadersLogging) { + logCdnHeaders(String.format("[segment/%s]", segmentName), _onDemandFetchMaxRetries + ON_DEMAND_FETCH_BACKOFF_MAX_RETRIES, + withCDNBypassed.remainingAttempts(), captor.get()); } } } diff --git a/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java b/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java index 63298a5e7..7c5fbe76e 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java +++ b/client/src/main/java/io/split/engine/experiments/SplitChangeFetcher.java @@ -1,6 +1,7 @@ package io.split.engine.experiments; import io.split.client.dtos.SplitChange; +import io.split.engine.common.FetchOptions; /** * Created by adilaijaz on 5/11/15. @@ -31,5 +32,5 @@ public interface SplitChangeFetcher { * @return SegmentChange * @throws java.lang.RuntimeException if there was a problem computing split changes */ - SplitChange fetch(long since, boolean addCacheHeader); + SplitChange fetch(long since, FetchOptions options); } diff --git a/client/src/main/java/io/split/engine/experiments/SplitFetcher.java b/client/src/main/java/io/split/engine/experiments/SplitFetcher.java index 4266659b1..78c3d0cc3 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitFetcher.java +++ b/client/src/main/java/io/split/engine/experiments/SplitFetcher.java @@ -1,5 +1,7 @@ package io.split.engine.experiments; +import io.split.engine.common.FetchOptions; + /** * Created by adilaijaz on 5/8/15. */ @@ -8,11 +10,11 @@ public interface SplitFetcher extends Runnable { * Forces a sync of splits, outside of any scheduled * syncs. This method MUST NOT throw any exceptions. */ - void forceRefresh(boolean addCacheHeader); + void forceRefresh(FetchOptions options); /** * Forces a sync of ALL splits, outside of any scheduled * syncs. This method MUST NOT throw any exceptions. */ - void fetchAll(boolean addCacheHeader); + boolean fetchAll(FetchOptions options); } 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 510001153..37ec85c6f 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java +++ b/client/src/main/java/io/split/engine/experiments/SplitFetcherImp.java @@ -5,6 +5,10 @@ import io.split.client.dtos.Status; import io.split.engine.SDKReadinessGates; import io.split.cache.SplitCache; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.engine.common.FetchOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -22,8 +26,8 @@ public class SplitFetcherImp implements SplitFetcher { private final SplitParser _parser; private final SplitChangeFetcher _splitChangeFetcher; private final SplitCache _splitCache; - private final SDKReadinessGates _gates; private final Object _lock = new Object(); + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; /** * Contains all the traffic types that are currently being used by the splits and also the count @@ -35,22 +39,30 @@ 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, SDKReadinessGates gates, SplitCache splitCache) { + public SplitFetcherImp(SplitChangeFetcher splitChangeFetcher, SplitParser parser, SplitCache splitCache, TelemetryRuntimeProducer telemetryRuntimeProducer) { _splitChangeFetcher = checkNotNull(splitChangeFetcher); _parser = checkNotNull(parser); - _gates = checkNotNull(gates); _splitCache = checkNotNull(splitCache); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } @Override - public void forceRefresh(boolean addCacheHeader) { + public void forceRefresh(FetchOptions options) { _log.debug("Force Refresh splits starting ..."); + final long INITIAL_CN = _splitCache.getChangeNumber(); try { while (true) { long start = _splitCache.getChangeNumber(); - runWithoutExceptionHandling(addCacheHeader); + runWithoutExceptionHandling(options); long end = _splitCache.getChangeNumber(); + // If the previous execution was the first one, clear the `cdnBypass` flag + // for the next fetches. (This will clear a local copy of the fetch options, + // not the original object that was passed to this method). + if (INITIAL_CN == start) { + options = new FetchOptions.Builder(options).targetChangeNumber(FetchOptions.DEFAULT_TARGET_CHANGENUMBER).build(); + } + if (start >= end) { break; } @@ -65,11 +77,12 @@ public void forceRefresh(boolean addCacheHeader) { @Override public void run() { - this.fetchAll(false); + this.fetchAll(new FetchOptions.Builder().cacheControlHeaders(false).build()); } - private void runWithoutExceptionHandling(boolean addCacheHeader) throws InterruptedException { - SplitChange change = _splitChangeFetcher.fetch(_splitCache.getChangeNumber(), addCacheHeader); + private void runWithoutExceptionHandling(FetchOptions options) throws InterruptedException { + long initTime = System.currentTimeMillis(); + SplitChange change = _splitChangeFetcher.fetch(_splitCache.getChangeNumber(), options); if (change == null) { throw new IllegalStateException("SplitChange was null"); @@ -136,23 +149,26 @@ private void runWithoutExceptionHandling(boolean addCacheHeader) throws Interrup } _splitCache.setChangeNumber(change.till); + _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SPLITS, System.currentTimeMillis()); } } @Override - public void fetchAll(boolean addCacheHeader) { + public boolean fetchAll(FetchOptions options) { _log.debug("Fetch splits starting ..."); long start = _splitCache.getChangeNumber(); try { - runWithoutExceptionHandling(addCacheHeader); - _gates.splitsAreReady(); + runWithoutExceptionHandling(options); + return true; } catch (InterruptedException e) { _log.warn("Interrupting split fetcher task"); Thread.currentThread().interrupt(); + return false; } catch (Throwable t) { _log.error("RefreshableSplitFetcher failed: " + t.getMessage()); if (_log.isDebugEnabled()) { _log.debug("Reason:", t); } + return false; } finally { if (_log.isDebugEnabled()) { _log.debug("split fetch before: " + start + ", after: " + _splitCache.getChangeNumber()); 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 e58292092..a5af700ff 100644 --- a/client/src/main/java/io/split/engine/experiments/SplitParser.java +++ b/client/src/main/java/io/split/engine/experiments/SplitParser.java @@ -43,7 +43,6 @@ */ public final class SplitParser { - public static final int CONDITIONS_UPPER_LIMIT = 50; private static final Logger _log = LoggerFactory.getLogger(SplitParser.class); private final SegmentSynchronizationTask _segmentSynchronizationTask; @@ -69,12 +68,6 @@ private ParsedSplit parseWithoutExceptionHandling(Split split) { return null; } - if (split.conditions.size() > CONDITIONS_UPPER_LIMIT) { - _log.warn(String.format("Dropping Split name=%s due to large number of conditions(%d)", - split.name, split.conditions.size())); - return null; - } - List parsedConditionList = Lists.newArrayList(); for (Condition condition : split.conditions) { diff --git a/client/src/main/java/io/split/engine/segments/SegmentChangeFetcher.java b/client/src/main/java/io/split/engine/segments/SegmentChangeFetcher.java index f4d46ed13..55fd23266 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentChangeFetcher.java +++ b/client/src/main/java/io/split/engine/segments/SegmentChangeFetcher.java @@ -1,6 +1,7 @@ package io.split.engine.segments; import io.split.client.dtos.SegmentChange; +import io.split.engine.common.FetchOptions; /** * Fetches changes in the segment since a reference point. @@ -22,8 +23,9 @@ public interface SegmentChangeFetcher { * @param segmentName the name of the segment to fetch. * @param changesSinceThisChangeNumber a value less than zero implies that the client is * requesting information on this segment for the first time. + * @param options * @return SegmentChange * @throws java.lang.RuntimeException if there was a problem fetching segment changes */ - SegmentChange fetch(String segmentName, long changesSinceThisChangeNumber, boolean addCacheHeader); + SegmentChange fetch(String segmentName, long changesSinceThisChangeNumber, FetchOptions options); } 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 af4bbc767..05cb511b2 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentFetcher.java +++ b/client/src/main/java/io/split/engine/segments/SegmentFetcher.java @@ -1,5 +1,7 @@ package io.split.engine.segments; +import io.split.engine.common.FetchOptions; + /** * Created by adilaijaz on 5/7/15. */ @@ -7,9 +9,9 @@ public interface SegmentFetcher { /** * fetch */ - void fetch(boolean addCacheHeader); + void fetch(FetchOptions opts); - void runWhitCacheHeader(); + boolean runWhitCacheHeader(); void fetchAll(); } 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 ac21e8461..13dfac4bb 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java +++ b/client/src/main/java/io/split/engine/segments/SegmentFetcherImp.java @@ -1,8 +1,13 @@ package io.split.engine.segments; +import com.google.common.annotations.VisibleForTesting; import io.split.cache.SegmentCache; import io.split.client.dtos.SegmentChange; import io.split.engine.SDKReadinessGates; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.engine.common.FetchOptions; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -18,22 +23,24 @@ public class SegmentFetcherImp implements SegmentFetcher { private final SegmentChangeFetcher _segmentChangeFetcher; private final SegmentCache _segmentCache; private final SDKReadinessGates _gates; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; private final Object _lock = new Object(); - public SegmentFetcherImp(String segmentName, SegmentChangeFetcher segmentChangeFetcher, SDKReadinessGates gates, SegmentCache segmentCache) { + public SegmentFetcherImp(String segmentName, SegmentChangeFetcher segmentChangeFetcher, SDKReadinessGates gates, SegmentCache segmentCache, TelemetryRuntimeProducer telemetryRuntimeProducer) { _segmentName = checkNotNull(segmentName); _segmentChangeFetcher = checkNotNull(segmentChangeFetcher); _segmentCache = checkNotNull(segmentCache); _gates = checkNotNull(gates); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); _segmentCache.updateSegment(segmentName, new ArrayList<>(), new ArrayList<>()); } @Override - public void fetch(boolean addCacheHeader){ + public void fetch(FetchOptions opts){ try { - callLoopRun(false, addCacheHeader); + callLoopRun(opts); } catch (Throwable t) { _log.error("RefreshableSegmentFetcher failed: " + t.getMessage()); if (_log.isDebugEnabled()) { @@ -42,8 +49,9 @@ public void fetch(boolean addCacheHeader){ } } - private void runWithoutExceptionHandling(boolean addCacheHeader) { - SegmentChange change = _segmentChangeFetcher.fetch(_segmentName, _segmentCache.getChangeNumber(_segmentName), addCacheHeader); + private void runWithoutExceptionHandling(FetchOptions options) { + long initTime = System.currentTimeMillis(); + SegmentChange change = _segmentChangeFetcher.fetch(_segmentName, _segmentCache.getChangeNumber(_segmentName), options); if (change == null) { throw new IllegalStateException("SegmentChange was null"); @@ -86,6 +94,7 @@ private void runWithoutExceptionHandling(boolean addCacheHeader) { } _segmentCache.setChangeNumber(_segmentName,change.till); + _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.SEGMENTS, System.currentTimeMillis()); } } @@ -109,14 +118,16 @@ private String summarize(List changes) { return bldr.toString(); } - private void callLoopRun(boolean isFetch, boolean addCacheHeader){ + @VisibleForTesting + void callLoopRun(FetchOptions opts){ + final long INITIAL_CN = _segmentCache.getChangeNumber(_segmentName); while (true) { long start = _segmentCache.getChangeNumber(_segmentName); - runWithoutExceptionHandling(addCacheHeader); - long end = _segmentCache.getChangeNumber(_segmentName); - if (isFetch && _log.isDebugEnabled()) { - _log.debug(_segmentName + " segment fetch before: " + start + ", after: " + _segmentCache.getChangeNumber(_segmentName) /*+ " size: " + _concurrentKeySet.size()*/); + runWithoutExceptionHandling(opts); + if (INITIAL_CN == start) { + opts = new FetchOptions.Builder(opts).targetChangeNumber(FetchOptions.DEFAULT_TARGET_CHANGENUMBER).build(); } + long end = _segmentCache.getChangeNumber(_segmentName); if (start >= end) { break; } @@ -124,34 +135,32 @@ private void callLoopRun(boolean isFetch, boolean addCacheHeader){ } @Override - public void runWhitCacheHeader(){ - this.fetchAndUpdate(true); + public boolean runWhitCacheHeader(){ + return this.fetchAndUpdate(new FetchOptions.Builder().cacheControlHeaders(true).build()); } /** * Calls callLoopRun and after fetchs segment. - * @param addCacheHeader indicates if CacheHeader is required + * @param opts contains all soft of options used when issuing the fetch request */ - private void fetchAndUpdate(boolean addCacheHeader) { + @VisibleForTesting + boolean fetchAndUpdate(FetchOptions opts) { try { // Do this again in case the previous call errored out. - _gates.registerSegment(_segmentName); - callLoopRun(true, addCacheHeader); - - _gates.segmentIsReady(_segmentName); + callLoopRun(opts); + return true; } catch (Throwable t) { _log.error("RefreshableSegmentFetcher failed: " + t.getMessage()); if (_log.isDebugEnabled()) { _log.debug("Reason:", t); } + return false; } } @Override public void fetchAll() { - this.fetchAndUpdate(false); + this.fetchAndUpdate(new FetchOptions.Builder().build()); } - - } diff --git a/client/src/main/java/io/split/engine/segments/SegmentImp.java b/client/src/main/java/io/split/engine/segments/SegmentImp.java index 2d153d1f3..d62cb1100 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentImp.java +++ b/client/src/main/java/io/split/engine/segments/SegmentImp.java @@ -41,4 +41,8 @@ public void update(List toAdd, List toRemove){ public boolean contains(String key) { return _concurrentKeySet.contains(key); } + + public long getKeysSize() { + return _concurrentKeySet.size(); + } } diff --git a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTask.java b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTask.java index 0bed99225..1a1764ed9 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTask.java +++ b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTask.java @@ -29,4 +29,9 @@ public interface SegmentSynchronizationTask extends Runnable { * @param addCacheHeader */ void fetchAll(boolean addCacheHeader); + + /** + * fetch every Segment Synchronous + */ + boolean fetchAllSynchronous(); } diff --git a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java index 7f3931f98..025ca08ab 100644 --- a/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java +++ b/client/src/main/java/io/split/engine/segments/SegmentSynchronizationTaskImp.java @@ -4,6 +4,7 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import io.split.cache.SegmentCache; import io.split.engine.SDKReadinessGates; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -11,13 +12,14 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; -import java.util.concurrent.Executors; -import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; @@ -33,10 +35,12 @@ public class SegmentSynchronizationTaskImp implements SegmentSynchronizationTask private final SegmentCache _segmentCache; private final SDKReadinessGates _gates; private final ScheduledExecutorService _scheduledExecutorService; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; private ScheduledFuture _scheduledFuture; - public SegmentSynchronizationTaskImp(SegmentChangeFetcher segmentChangeFetcher, long refreshEveryNSeconds, int numThreads, SDKReadinessGates gates, SegmentCache segmentCache) { + public SegmentSynchronizationTaskImp(SegmentChangeFetcher segmentChangeFetcher, long refreshEveryNSeconds, int numThreads, SDKReadinessGates gates, SegmentCache segmentCache, + TelemetryRuntimeProducer telemetryRuntimeProducer) { _segmentChangeFetcher = checkNotNull(segmentChangeFetcher); checkArgument(refreshEveryNSeconds >= 0L); @@ -54,6 +58,7 @@ public SegmentSynchronizationTaskImp(SegmentChangeFetcher segmentChangeFetcher, _running = new AtomicBoolean(false); _segmentCache = checkNotNull(segmentCache); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } @Override @@ -77,13 +82,7 @@ public void initializeSegment(String segmentName) { return; } - try { - _gates.registerSegment(segmentName); - } catch (InterruptedException e) { - _log.error("Unable to register segment " + segmentName); - } - - segment = new SegmentFetcherImp(segmentName, _segmentChangeFetcher, _gates, _segmentCache); + segment = new SegmentFetcherImp(segmentName, _segmentChangeFetcher, _gates, _segmentCache, _telemetryRuntimeProducer); if (_running.get()) { _scheduledExecutorService.submit(segment::fetchAll); @@ -102,7 +101,7 @@ public SegmentFetcher getFetcher(String segmentName) { @Override public void startPeriodicFetching() { - if (_running.getAndSet(true)) { + if (_running.getAndSet(true) ) { _log.debug("Segments PeriodicFetching is running..."); return; } @@ -154,7 +153,27 @@ public void fetchAll(boolean addCacheHeader) { _scheduledExecutorService.submit(fetcher::runWhitCacheHeader); continue; } + _scheduledExecutorService.submit(fetcher::fetchAll); } } + + @Override + public boolean fetchAllSynchronous() { + AtomicBoolean fetchAllStatus = new AtomicBoolean(true); + _segmentFetchers + .entrySet() + .stream().map(e -> _scheduledExecutorService.submit(e.getValue()::runWhitCacheHeader)) + .collect(Collectors.toList()) + .stream().forEach(future -> { + try { + if(!future.get()) { + fetchAllStatus.set(false); + }; + } catch (Exception ex) { + fetchAllStatus.set(false); + _log.error(ex.getMessage()); + }}); + return fetchAllStatus.get(); + } } diff --git a/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java b/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java index 2912c9ad1..6b4971cc4 100644 --- a/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java +++ b/client/src/main/java/io/split/engine/sse/AuthApiClientImp.java @@ -4,6 +4,9 @@ import io.split.client.utils.Json; import io.split.engine.sse.dtos.AuthenticationResponse; import io.split.engine.sse.dtos.RawAuthResponse; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; @@ -23,16 +26,18 @@ public class AuthApiClientImp implements AuthApiClient { private final CloseableHttpClient _httpClient; private final String _target; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; - public AuthApiClientImp(String url, - CloseableHttpClient httpClient) { + public AuthApiClientImp(String url, CloseableHttpClient httpClient, TelemetryRuntimeProducer telemetryRuntimeProducer) { _httpClient = checkNotNull(httpClient); _target = checkNotNull(url); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } @Override public AuthenticationResponse Authenticate() { try { + long initTime = System.currentTimeMillis(); URI uri = new URIBuilder(_target).build(); HttpGet request = new HttpGet(uri); @@ -43,11 +48,17 @@ public AuthenticationResponse Authenticate() { _log.debug(String.format("Success connection to: %s", _target)); String jsonContent = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + _telemetryRuntimeProducer.recordTokenRefreshes(); + _telemetryRuntimeProducer.recordSuccessfulSync(LastSynchronizationRecordsEnum.TOKEN, System.currentTimeMillis()); + _telemetryRuntimeProducer.recordSyncLatency(HTTPLatenciesEnum.TOKEN, System.currentTimeMillis()-initTime); return getSuccessResponse(jsonContent); } _log.error(String.format("Problem to connect to : %s. Response status: %s", _target, statusCode)); if (statusCode >= HttpStatus.SC_BAD_REQUEST && statusCode < HttpStatus.SC_INTERNAL_SERVER_ERROR) { + if (statusCode == HttpStatus.SC_UNAUTHORIZED) { + _telemetryRuntimeProducer.recordAuthRejections(); + } return new AuthenticationResponse(false,false); } 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 7d8bf990d..772ccfb48 100644 --- a/client/src/main/java/io/split/engine/sse/EventSourceClientImp.java +++ b/client/src/main/java/io/split/engine/sse/EventSourceClientImp.java @@ -7,6 +7,7 @@ import io.split.engine.sse.exceptions.EventParsingException; import io.split.engine.sse.workers.SplitsWorker; import io.split.engine.sse.workers.Worker; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.core5.net.URIBuilder; import org.slf4j.Logger; @@ -35,7 +36,8 @@ public class EventSourceClientImp implements EventSourceClient { NotificationParser notificationParser, NotificationProcessor notificationProcessor, PushStatusTracker pushStatusTracker, - CloseableHttpClient sseHttpClient) { + CloseableHttpClient sseHttpClient, + TelemetryRuntimeProducer telemetryRuntimeProducer) { _baseStreamingUrl = checkNotNull(baseStreamingUrl); _notificationParser = checkNotNull(notificationParser); _notificationProcessor = checkNotNull(notificationProcessor); @@ -44,7 +46,7 @@ public class EventSourceClientImp implements EventSourceClient { _sseClient = new SSEClient( inboundEvent -> { onMessage(inboundEvent); return null; }, status -> { _pushStatusTracker.handleSseStatus(status); return null; }, - sseHttpClient); + sseHttpClient, telemetryRuntimeProducer); _firstEvent = new AtomicBoolean(); } @@ -52,12 +54,13 @@ public static EventSourceClientImp build(String baseStreamingUrl, SplitsWorker splitsWorker, Worker segmentWorker, PushStatusTracker pushStatusTracker, - CloseableHttpClient sseHttpClient) { + CloseableHttpClient sseHttpClient, TelemetryRuntimeProducer telemetryRuntimeProducer) { return new EventSourceClientImp(baseStreamingUrl, new NotificationParserImp(), NotificationProcessorImp.build(splitsWorker, segmentWorker, pushStatusTracker), pushStatusTracker, - sseHttpClient); + sseHttpClient, + telemetryRuntimeProducer); } @Override diff --git a/client/src/main/java/io/split/engine/sse/PushStatusTrackerImp.java b/client/src/main/java/io/split/engine/sse/PushStatusTrackerImp.java index f76cf691e..b7e04730f 100644 --- a/client/src/main/java/io/split/engine/sse/PushStatusTrackerImp.java +++ b/client/src/main/java/io/split/engine/sse/PushStatusTrackerImp.java @@ -7,6 +7,9 @@ import io.split.engine.sse.dtos.ControlType; import io.split.engine.sse.dtos.ErrorNotification; import io.split.engine.sse.dtos.OccupancyNotification; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.enums.StreamEventsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,8 +18,12 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; +import static com.google.common.base.Preconditions.checkNotNull; + public class PushStatusTrackerImp implements PushStatusTracker { private static final Logger _log = LoggerFactory.getLogger(PushStatusTracker.class); + private static final String CONTROL_PRI_CHANNEL = "control_pri"; + private static final String CONTROL_SEC_CHANNEL = "control_sec"; private final AtomicBoolean _publishersOnline = new AtomicBoolean(true); private final AtomicReference _sseStatus = new AtomicReference<>(SSEClient.StatusMessage.INITIALIZATION_IN_PROGRESS); @@ -24,8 +31,11 @@ public class PushStatusTrackerImp implements PushStatusTracker { private final LinkedBlockingQueue _statusMessages; private final ConcurrentMap regions = Maps.newConcurrentMap(); - public PushStatusTrackerImp(LinkedBlockingQueue statusMessages) { + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + + public PushStatusTrackerImp(LinkedBlockingQueue statusMessages, TelemetryRuntimeProducer telemetryRuntimeProducer) { _statusMessages = statusMessages; + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } private synchronized void reset() { @@ -42,6 +52,7 @@ public void handleSseStatus(SSEClient.StatusMessage newStatus) { case FIRST_EVENT: if (SSEClient.StatusMessage.CONNECTED.equals(_sseStatus.get())) { _statusMessages.offer(PushManager.Status.STREAMING_READY); + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.CONNECTION_ESTABLISHED.getType(),0l, System.currentTimeMillis())); } case CONNECTED: _sseStatus.compareAndSet(SSEClient.StatusMessage.INITIALIZATION_IN_PROGRESS, SSEClient.StatusMessage.CONNECTED); @@ -85,12 +96,14 @@ public void handleIncomingControlEvent(ControlNotification controlNotification) } break; case STREAMING_PAUSED: + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.STREAMING_STATUS.getType(), StreamEventsEnum.StreamingStatusValues.STREAMING_PAUSED.getValue(), System.currentTimeMillis())); if (_backendStatus.compareAndSet(ControlType.STREAMING_RESUMED, ControlType.STREAMING_PAUSED) && _publishersOnline.get()) { // If there are no publishers online, the STREAMING_DOWN message should have already been sent _statusMessages.offer(PushManager.Status.STREAMING_DOWN); } break; case STREAMING_DISABLED: + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.STREAMING_STATUS.getType(), StreamEventsEnum.StreamingStatusValues.STREAMING_DISABLED.getValue(), System.currentTimeMillis())); _backendStatus.set(ControlType.STREAMING_DISABLED); _statusMessages.offer(PushManager.Status.STREAMING_OFF); break; @@ -102,6 +115,7 @@ public void handleIncomingOccupancyEvent(OccupancyNotification occupancyNotifica _log.debug(String.format("handleIncomingOccupancyEvent: publishers=%d", occupancyNotification.getMetrics().getPublishers())); int publishers = occupancyNotification.getMetrics().getPublishers(); + recordTelemetryOcuppancy(occupancyNotification, publishers); regions.put(occupancyNotification.getChannel(), publishers); boolean isPublishers = isPublishers(); if (!isPublishers && _publishersOnline.compareAndSet(true, false) && _backendStatus.get().equals(ControlType.STREAMING_RESUMED)) { @@ -114,7 +128,7 @@ public void handleIncomingOccupancyEvent(OccupancyNotification occupancyNotifica @Override public void handleIncomingAblyError(ErrorNotification notification) { _log.debug(String.format("handleIncomingAblyError: %s", notification.getMessage())); - + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.ABLY_ERROR.getType(), notification.getCode(), System.currentTimeMillis())); if (_backendStatus.get().equals(ControlType.STREAMING_DISABLED)) { return; // Ignore } @@ -145,4 +159,14 @@ private boolean isPublishers() { } return false; } + + private void recordTelemetryOcuppancy(OccupancyNotification occupancyNotification, int publishers) { + if (CONTROL_PRI_CHANNEL.equals(occupancyNotification.getChannel())) { + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.OCCUPANCY_PRI.getType(), publishers, System.currentTimeMillis())); + } + else if (CONTROL_SEC_CHANNEL.equals(occupancyNotification.getChannel())){ + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.OCCUPANCY_SEC.getType(), publishers, System.currentTimeMillis())); + } + + } } \ No newline at end of file diff --git a/client/src/main/java/io/split/engine/sse/client/SSEClient.java b/client/src/main/java/io/split/engine/sse/client/SSEClient.java index 6f072400d..abb21fee5 100644 --- a/client/src/main/java/io/split/engine/sse/client/SSEClient.java +++ b/client/src/main/java/io/split/engine/sse/client/SSEClient.java @@ -2,6 +2,9 @@ import com.google.common.base.Strings; import com.google.common.util.concurrent.ThreadFactoryBuilder; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.enums.StreamEventsEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.classic.methods.HttpGet; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; @@ -58,13 +61,17 @@ private enum ConnectionState { private final AtomicReference _ongoingRequest = new AtomicReference<>(); private AtomicBoolean _forcedStop; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + public SSEClient(Function eventCallback, Function statusCallback, - CloseableHttpClient client) { + CloseableHttpClient client, + TelemetryRuntimeProducer telemetryRuntimeProducer) { _eventCallback = eventCallback; _statusCallback = statusCallback; _client = client; _forcedStop = new AtomicBoolean(); + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); } public synchronized boolean open(URI uri) { @@ -126,20 +133,27 @@ private void connectAndLoop(URI uri, CountDownLatch signal) { _log.debug(exc.getMessage()); if (SOCKET_CLOSED_MESSAGE.equals(exc.getMessage())) { // Connection closed by us _statusCallback.apply(StatusMessage.FORCED_STOP); + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); return; } // Connection closed by server _statusCallback.apply(StatusMessage.RETRYABLE_ERROR); + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); return; } catch (IOException exc) { // Other type of connection error if(!_forcedStop.get()) { _log.debug(String.format("SSE connection ended abruptly: %s. Retying", exc.getMessage())); + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), StreamEventsEnum.SseConnectionErrorValues.REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); _statusCallback.apply(StatusMessage.RETRYABLE_ERROR); return; } + + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); } } } catch (Exception e) { // Any other error non related to the connection disables streaming altogether + + _telemetryRuntimeProducer.recordStreamingEvents(new StreamingEvent(StreamEventsEnum.SSE_CONNECTION_ERROR.getType(), StreamEventsEnum.SseConnectionErrorValues.NON_REQUESTED_CONNECTION_ERROR.getValue(), System.currentTimeMillis())); _log.warn(e.getMessage(), e); _statusCallback.apply(StatusMessage.NONRETRYABLE_ERROR); } finally { @@ -156,7 +170,6 @@ private void connectAndLoop(URI uri, CountDownLatch signal) { } private boolean establishConnection(URI uri, CountDownLatch signal) { - _ongoingRequest.set(new HttpGet(uri)); try { diff --git a/client/src/main/java/io/split/engine/sse/workers/Worker.java b/client/src/main/java/io/split/engine/sse/workers/Worker.java index d6b8db5d4..7d2dd21ab 100644 --- a/client/src/main/java/io/split/engine/sse/workers/Worker.java +++ b/client/src/main/java/io/split/engine/sse/workers/Worker.java @@ -25,6 +25,7 @@ public void start() { _log.debug(String.format("%s Worker starting ...", _workerName)); _queue.clear(); _thread = new Thread( this); + _thread.setName(String.format("%s-worker", _workerName)); _thread.start(); } else { _log.debug(String.format("%s Worker already running.", _workerName)); diff --git a/client/src/main/java/io/split/service/HttpPostImp.java b/client/src/main/java/io/split/service/HttpPostImp.java new file mode 100644 index 000000000..e0586eb47 --- /dev/null +++ b/client/src/main/java/io/split/service/HttpPostImp.java @@ -0,0 +1,51 @@ +package io.split.service; + +import io.split.client.utils.Utils; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; +import org.apache.hc.core5.http.HttpEntity; +import org.apache.hc.core5.http.HttpStatus; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.hc.client5.http.classic.methods.HttpPost; + +import java.net.URI; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class HttpPostImp { + private static final Logger _logger = LoggerFactory.getLogger(HttpPostImp.class); + private CloseableHttpClient _client; + private final TelemetryRuntimeProducer _telemetryRuntimeProducer; + + public HttpPostImp(CloseableHttpClient client, TelemetryRuntimeProducer telemetryRuntimeProducer) { + _client = client; + _telemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + } + + public void post(URI uri, Object object, String posted, HTTPLatenciesEnum httpLatenciesEnum, LastSynchronizationRecordsEnum lastSynchronizationRecordsEnum, ResourceEnum resourceEnum) { + long initTime = System.currentTimeMillis(); + HttpEntity entity = Utils.toJsonEntity(object); + HttpPost request = new HttpPost(uri); + request.setEntity(entity); + + try (CloseableHttpResponse response = _client.execute(request)) { + + int status = response.getCode(); + + if (status < HttpStatus.SC_OK || status >= HttpStatus.SC_MULTIPLE_CHOICES) { + _telemetryRuntimeProducer.recordSyncError(resourceEnum, status); + _logger.warn("Response status was: " + status); + return; + } + _telemetryRuntimeProducer.recordSyncLatency(httpLatenciesEnum, System.currentTimeMillis() - initTime); + _telemetryRuntimeProducer.recordSuccessfulSync(lastSynchronizationRecordsEnum, System.currentTimeMillis()); + } catch (Throwable t) { + _logger.warn("Exception when posting " + posted + object, t); + } + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/Config.java b/client/src/main/java/io/split/telemetry/domain/Config.java new file mode 100644 index 000000000..1844b8e98 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/Config.java @@ -0,0 +1,196 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class Config { + /* package private */ static final String FIELD_OPERATION_MODE = "oM"; + /* package private */ static final String FIELD_STREAMING_ENABLED = "sE"; + /* package private */ static final String FIELD_STORAGE = "st"; + /* package private */ static final String FIELD_RATES = "rR"; + /* package private */ static final String FIELD_URL_OVERRIDES = "uO"; + /* package private */ static final String FIELD_IMPRESSIONS_QUEUE = "iQ"; + /* package private */ static final String FIELD_EVENT_QUEUE = "eQ"; + /* package private */ static final String FIELD_IMPRESSIONS_MODE = "iM"; + /* package private */ static final String FIELD_IMPRESSIONS_LISTENER = "iL"; + /* package private */ static final String FIELD_HTTP_PROXY_DETECTED = "hP"; + /* package private */ static final String FIELD_ACTIVE_FACTORIES = "aF"; + /* package private */ static final String FIELD_REDUNDANT_FACTORIES = "rF"; + /* package private */ static final String FIELD_TIME_UNTIL_READY = "tR"; + /* package private */ static final String FIELD_BUR_TIMEOUTS = "bT"; + /* package private */ static final String FIELD_NON_READY_USAGES = "nR"; + /* package private */ static final String FIELD_INTEGRATIONS = "i"; + /* package private */ static final String FIELD__TAGS = "t"; + + @SerializedName(FIELD_OPERATION_MODE) + private int _operationMode; + @SerializedName(FIELD_STREAMING_ENABLED) + private boolean _streamingEnabled; + @SerializedName(FIELD_STORAGE) + private String _storage; + @SerializedName(FIELD_RATES) + private Rates _rates; + @SerializedName(FIELD_URL_OVERRIDES) + private URLOverrides _urlOverrides; + @SerializedName(FIELD_IMPRESSIONS_QUEUE) + private long _impressionsQueueSize; + @SerializedName(FIELD_EVENT_QUEUE) + private long _eventsQueueSize; + @SerializedName(FIELD_IMPRESSIONS_MODE) + private int _impressionsMode; + @SerializedName(FIELD_IMPRESSIONS_LISTENER) + private boolean _impressionsListenerEnabled; + @SerializedName(FIELD_HTTP_PROXY_DETECTED) + private boolean _httpProxyDetected; + @SerializedName(FIELD_ACTIVE_FACTORIES) + private long _activeFactories; + @SerializedName(FIELD_REDUNDANT_FACTORIES) + private long _redundantFactories; + @SerializedName(FIELD_TIME_UNTIL_READY) + private long _timeUntilReady; + @SerializedName(FIELD_BUR_TIMEOUTS) + private long _burTimeouts; + @SerializedName(FIELD_NON_READY_USAGES) + private long _nonReadyUsages; + @SerializedName(FIELD_INTEGRATIONS) + private List _integrations; + @SerializedName(FIELD__TAGS) + private List _tags; + + public int get_operationMode() { + return _operationMode; + } + + public void set_operationMode(int _operationMode) { + this._operationMode = _operationMode; + } + + public boolean is_streamingEnabled() { + return _streamingEnabled; + } + + public void set_streamingEnabled(boolean _streamingEnabled) { + this._streamingEnabled = _streamingEnabled; + } + + public String get_storage() { + return _storage; + } + + public void set_storage(String _storage) { + this._storage = _storage; + } + + public Rates get_rates() { + return _rates; + } + + public void set_rates(Rates _rates) { + this._rates = _rates; + } + + public URLOverrides get_urlOverrides() { + return _urlOverrides; + } + + public void set_urlOverrides(URLOverrides _urlOverrides) { + this._urlOverrides = _urlOverrides; + } + + public long get_impressionsQueueSize() { + return _impressionsQueueSize; + } + + public void set_impressionsQueueSize(long _impressionsQueueSize) { + this._impressionsQueueSize = _impressionsQueueSize; + } + + public long get_eventsQueueSize() { + return _eventsQueueSize; + } + + public void set_eventsQueueSize(long _eventsQueueSize) { + this._eventsQueueSize = _eventsQueueSize; + } + + public int get_impressionsMode() { + return _impressionsMode; + } + + public void set_impressionsMode(int _impressionsMode) { + this._impressionsMode = _impressionsMode; + } + + public boolean is_impressionsListenerEnabled() { + return _impressionsListenerEnabled; + } + + public void set_impressionsListenerEnabled(boolean _impressionsListenerEnabled) { + this._impressionsListenerEnabled = _impressionsListenerEnabled; + } + + public boolean is_httpProxyDetected() { + return _httpProxyDetected; + } + + public void set_httpProxyDetected(boolean _httpProxyDetected) { + this._httpProxyDetected = _httpProxyDetected; + } + + public long get_activeFactories() { + return _activeFactories; + } + + public void set_activeFactories(long _activeFactories) { + this._activeFactories = _activeFactories; + } + + public long get_redundantFactories() { + return _redundantFactories; + } + + public void set_redundantFactories(long _redundantFactories) { + this._redundantFactories = _redundantFactories; + } + + public long get_timeUntilReady() { + return _timeUntilReady; + } + + public void set_timeUntilReady(long _timeUntilReady) { + this._timeUntilReady = _timeUntilReady; + } + + public long get_burTimeouts() { + return _burTimeouts; + } + + public void set_burTimeouts(long _burTimeouts) { + this._burTimeouts = _burTimeouts; + } + + public long get_nonReadyUsages() { + return _nonReadyUsages; + } + + public void set_nonReadyUsages(long _nonReadyUsages) { + this._nonReadyUsages = _nonReadyUsages; + } + + public List get_integrations() { + return _integrations; + } + + public void set_integrations(List _integrations) { + this._integrations = _integrations; + } + + public List get_tags() { + return _tags; + } + + public void set_tags(List _tags) { + this._tags = _tags; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/HTTPErrors.java b/client/src/main/java/io/split/telemetry/domain/HTTPErrors.java new file mode 100644 index 000000000..dac746117 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/HTTPErrors.java @@ -0,0 +1,97 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +public class HTTPErrors { + /* package private */ static final String FIELD_SPLIT = "sp"; + /* package private */ static final String FIELD_SEGMENTS = "se"; + /* package private */ static final String FIELD_IMPRESSIONS = "im"; + /* package private */ static final String FIELD_IMPRESSIONS_COUNT = "ic"; + /* package private */ static final String FIELD_EVENTS = "ev"; + /* package private */ static final String FIELD_TOKEN = "to"; + /* package private */ static final String FIELD_TELEMETRY = "te"; + + @SerializedName(FIELD_SPLIT) + private Map _splits; + @SerializedName(FIELD_SEGMENTS) + private Map _segments; + @SerializedName(FIELD_IMPRESSIONS) + private Map _impressions; + @SerializedName(FIELD_IMPRESSIONS_COUNT) + private Map _impressionsCount; + @SerializedName(FIELD_EVENTS) + private Map _events; + @SerializedName(FIELD_TOKEN) + private Map _token; + @SerializedName(FIELD_TELEMETRY) + private Map _telemetry; + + public HTTPErrors() { + _splits = new ConcurrentHashMap<>(); + _segments = new ConcurrentHashMap<>(); + _impressions = new ConcurrentHashMap<>(); + _impressionsCount = new ConcurrentHashMap<>(); + _events = new ConcurrentHashMap<>(); + _token = new ConcurrentHashMap<>(); + _telemetry = new ConcurrentHashMap<>(); + } + + public Map get_splits() { + return _splits; + } + + public void set_splits(Map _splits) { + this._splits = _splits; + } + + public Map get_segments() { + return _segments; + } + + public void set_segments(Map _segments) { + this._segments = _segments; + } + + public Map get_impressions() { + return _impressions; + } + + public void set_impressions(Map _impressions) { + this._impressions = _impressions; + } + + public Map get_events() { + return _events; + } + + public void set_events(Map _events) { + this._events = _events; + } + + public Map get_token() { + return _token; + } + + public void set_token(Map _token) { + this._token = _token; + } + + public Map get_telemetry() { + return _telemetry; + } + + public void set_telemetry(Map _telemetry) { + this._telemetry = _telemetry; + } + + public Map get_impressionsCount() { + return _impressionsCount; + } + + public void set_impressionsCount(Map _impressionsCount) { + this._impressionsCount = _impressionsCount; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/HTTPLatencies.java b/client/src/main/java/io/split/telemetry/domain/HTTPLatencies.java new file mode 100644 index 000000000..0e0791ed9 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/HTTPLatencies.java @@ -0,0 +1,97 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.List; + +public class HTTPLatencies { + /* package private */ static final String FIELD_SPLIT = "sp"; + /* package private */ static final String FIELD_SEGMENTS = "se"; + /* package private */ static final String FIELD_IMPRESSIONS = "im"; + /* package private */ static final String FIELD_IMPRESSIONS_COUNT = "ic"; + /* package private */ static final String FIELD_EVENTS = "ev"; + /* package private */ static final String FIELD_TOKEN = "to"; + /* package private */ static final String FIELD_TELEMETRY = "te"; + + @SerializedName(FIELD_SPLIT) + private List _splits; + @SerializedName(FIELD_SEGMENTS) + private List_segments; + @SerializedName(FIELD_IMPRESSIONS) + private List _impressions; + @SerializedName(FIELD_IMPRESSIONS_COUNT) + private List _impressionsCount; + @SerializedName(FIELD_EVENTS) + private List _events; + @SerializedName(FIELD_TOKEN) + private List _token; + @SerializedName(FIELD_TELEMETRY) + private List _telemetry; + + public HTTPLatencies() { + _splits = new ArrayList<>(); + _segments = new ArrayList<>(); + _impressions = new ArrayList<>(); + _impressionsCount = new ArrayList<>(); + _events = new ArrayList<>(); + _token = new ArrayList<>(); + _telemetry = new ArrayList<>(); + } + + public List get_splits() { + return _splits; + } + + public void set_splits(List _splits) { + this._splits = _splits; + } + + public List get_segments() { + return _segments; + } + + public void set_segments(List _segments) { + this._segments = _segments; + } + + public List get_impressions() { + return _impressions; + } + + public void set_impressions(List _impressions) { + this._impressions = _impressions; + } + + public List get_events() { + return _events; + } + + public void set_events(List _events) { + this._events = _events; + } + + public List get_token() { + return _token; + } + + public void set_token(List _token) { + this._token = _token; + } + + public List get_telemetry() { + return _telemetry; + } + + public void set_telemetry(List _telemetry) { + this._telemetry = _telemetry; + } + + public List get_impressionsCount() { + return _impressionsCount; + } + + public void set_impressionsCount(List _impressionsCount) { + this._impressionsCount = _impressionsCount; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/LastSynchronization.java b/client/src/main/java/io/split/telemetry/domain/LastSynchronization.java new file mode 100644 index 000000000..59586562e --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/LastSynchronization.java @@ -0,0 +1,84 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +public class LastSynchronization { + /* package private */ static final String FIELD_SPLIT = "sp"; + /* package private */ static final String FIELD_SEGMENTS = "se"; + /* package private */ static final String FIELD_IMPRESSIONS = "im"; + /* package private */ static final String FIELD_IMPRESSIONS_COUNT = "ic"; + /* package private */ static final String FIELD_EVENTS = "ev"; + /* package private */ static final String FIELD_TOKEN = "to"; + /* package private */ static final String FIELD_TELEMETRY = "te"; + + @SerializedName(FIELD_SPLIT) + private long _splits; + @SerializedName(FIELD_SEGMENTS) + private long _segments; + @SerializedName(FIELD_IMPRESSIONS) + private long _impressions; + @SerializedName(FIELD_IMPRESSIONS_COUNT) + private long _impressionsCount; + @SerializedName(FIELD_EVENTS) + private long _events; + @SerializedName(FIELD_TOKEN) + private long _token; + @SerializedName(FIELD_TELEMETRY) + private long _telemetry; + + public long get_splits() { + return _splits; + } + + public void set_splits(long _splits) { + this._splits = _splits; + } + + public long get_segments() { + return _segments; + } + + public void set_segments(long _segments) { + this._segments = _segments; + } + + public long get_impressions() { + return _impressions; + } + + public void set_impressions(long _impressions) { + this._impressions = _impressions; + } + + public long get_events() { + return _events; + } + + public void set_events(long _events) { + this._events = _events; + } + + public long get_token() { + return _token; + } + + public void set_token(long _token) { + this._token = _token; + } + + public long get_telemetry() { + return _telemetry; + } + + public void set_telemetry(long _telemetry) { + this._telemetry = _telemetry; + } + + public long get_impressionsCount() { + return _impressionsCount; + } + + public void set_impressionsCount(long _impressionsCount) { + this._impressionsCount = _impressionsCount; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/MethodExceptions.java b/client/src/main/java/io/split/telemetry/domain/MethodExceptions.java new file mode 100644 index 000000000..c6d6561be --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/MethodExceptions.java @@ -0,0 +1,62 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +public class MethodExceptions { + /* package private */ static final String FIELD_TREATMENT = "t"; + /* package private */ static final String FIELD_TREATMENTS = "ts"; + /* package private */ static final String FIELD_TREATMENT_WITH_CONFIG = "tc"; + /* package private */ static final String FIELD_TREATMENTS_WITH_CONFIG = "tcs"; + /* package private */ static final String FIELD_TRACK = "tr"; + + @SerializedName(FIELD_TREATMENT) + private long _treatment; + @SerializedName(FIELD_TREATMENTS) + private long _treatments; + @SerializedName(FIELD_TREATMENT_WITH_CONFIG) + private long _treatmentWithConfig; + @SerializedName(FIELD_TREATMENTS_WITH_CONFIG) + private long _treatmentsWithConfig; + @SerializedName(FIELD_TRACK) + private long _track; + + public long get_treatment() { + return _treatment; + } + + public void set_treatment(long _treatment) { + this._treatment = _treatment; + } + + public long get_treatments() { + return _treatments; + } + + public void set_treatments(long _treatments) { + this._treatments = _treatments; + } + + public long get_treatmentsWithConfig() { + return _treatmentsWithConfig; + } + + public void set_treatmentsWithConfig(long _treatmentsWithConfig) { + this._treatmentsWithConfig = _treatmentsWithConfig; + } + + public long get_treatmentWithConfig() { + return _treatmentWithConfig; + } + + public void set_treatmentWithConfig(long _treatmentWithConfig) { + this._treatmentWithConfig = _treatmentWithConfig; + } + + public long get_track() { + return _track; + } + + public void set_track(long _track) { + this._track = _track; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/MethodLatencies.java b/client/src/main/java/io/split/telemetry/domain/MethodLatencies.java new file mode 100644 index 000000000..21aae636c --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/MethodLatencies.java @@ -0,0 +1,73 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +import java.util.ArrayList; +import java.util.List; + +public class MethodLatencies { + /* package private */ static final String FIELD_TREATMENT = "t"; + /* package private */ static final String FIELD_TREATMENTS = "ts"; + /* package private */ static final String FIELD_TREATMENT_WITH_CONFIG = "tc"; + /* package private */ static final String FIELD_TREATMENTS_WITH_CONFIG = "tcs"; + /* package private */ static final String FIELD_TRACK = "tr"; + + @SerializedName(FIELD_TREATMENT) + private List _treatment; + @SerializedName(FIELD_TREATMENTS) + private List _treatments; + @SerializedName(FIELD_TREATMENT_WITH_CONFIG) + private List _treatmentWithConfig; + @SerializedName(FIELD_TREATMENTS_WITH_CONFIG) + private List _treatmentsWithConfig; + @SerializedName(FIELD_TRACK) + private List _track; + + public MethodLatencies() { + _treatment = new ArrayList<>(); + _treatments = new ArrayList<>(); + _treatmentWithConfig = new ArrayList<>(); + _treatmentsWithConfig = new ArrayList<>(); + _track = new ArrayList<>(); + } + + public List get_treatment() { + return _treatment; + } + + public void set_treatment(List _treatment) { + this._treatment = _treatment; + } + + public List get_treatments() { + return _treatments; + } + + public void set_treatments(List _treatments) { + this._treatments = _treatments; + } + + public List get_treatmentsWithConfig() { + return _treatmentsWithConfig; + } + + public void set_treatmentsWithConfig(List _treatmentsWithConfig) { + this._treatmentsWithConfig = _treatmentsWithConfig; + } + + public List get_treatmentWithConfig() { + return _treatmentWithConfig; + } + + public void set_treatmentWithConfig(List _treatmentWithConfig) { + this._treatmentWithConfig = _treatmentWithConfig; + } + + public List get_track() { + return _track; + } + + public void set_track(List _track) { + this._track = _track; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/Rates.java b/client/src/main/java/io/split/telemetry/domain/Rates.java new file mode 100644 index 000000000..e80d26079 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/Rates.java @@ -0,0 +1,62 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +public class Rates { + /* package private */ static final String FIELD_SPLITS = "sp"; + /* package private */ static final String FIELD_SEGMENTS = "se"; + /* package private */ static final String FIELD_IMPRESSIONS = "im"; + /* package private */ static final String FIELD_EVENTS = "ev"; + /* package private */ static final String FIELD_TELEMETRY = "te"; + + @SerializedName(FIELD_SPLITS) + private long _splits; + @SerializedName(FIELD_SEGMENTS) + private long _segments; + @SerializedName(FIELD_IMPRESSIONS) + private long _impressions; + @SerializedName(FIELD_EVENTS) + private long _events; + @SerializedName(FIELD_TELEMETRY) + private long _telemetry; + + public long get_splits() { + return _splits; + } + + public void set_splits(long _splits) { + this._splits = _splits; + } + + public long get_segments() { + return _segments; + } + + public void set_segments(long _segments) { + this._segments = _segments; + } + + public long get_impressions() { + return _impressions; + } + + public void set_impressions(long _impressions) { + this._impressions = _impressions; + } + + public long get_events() { + return _events; + } + + public void set_events(long _events) { + this._events = _events; + } + + public long get_telemetry() { + return _telemetry; + } + + public void set_telemetry(long _telemetry) { + this._telemetry = _telemetry; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/Stats.java b/client/src/main/java/io/split/telemetry/domain/Stats.java new file mode 100644 index 000000000..8baec7261 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/Stats.java @@ -0,0 +1,207 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +import java.util.List; + +public class Stats { + /* package private */ static final String FIELD_LAST_SYNCHRONIZATION = "lS"; + /* package private */ static final String FIELD_METHOD_LATENCIES = "ml"; + /* package private */ static final String FIELD_METHOD_EXCEPTIONS = "mE"; + /* package private */ static final String FIELD_HTTP_ERRORS = "hE"; + /* package private */ static final String FIELD_HTTP_LATENCIES = "hL"; + /* package private */ static final String FIELD_TOKEN_REFRESHES = "tR"; + /* package private */ static final String FIELD_AUTH_REJECTIONS = "aR"; + /* package private */ static final String FIELD_IMPRESSIONS_QUEUED = "iQ"; + /* package private */ static final String FIELD_IMPRESSIONS_DEDUPED = "iDe"; + /* package private */ static final String FIELD_IMPRESSIONS_DROPPED = "iDr"; + /* package private */ static final String FIELD_SPLITS = "spC"; + /* package private */ static final String FIELD_SEGMENTS = "seC"; + /* package private */ static final String FIELD_SEGMENTS_KEY = "skC"; + /* package private */ static final String FIELD_SESSION_LENGHT = "sL"; + /* package private */ static final String FIELD_EVENTS_QUEUED = "eQ"; + /* 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"; + + @SerializedName(FIELD_LAST_SYNCHRONIZATION) + private LastSynchronization _lastSynchronization; + @SerializedName(FIELD_METHOD_LATENCIES) + private MethodLatencies _methodLatencies; + @SerializedName(FIELD_METHOD_EXCEPTIONS) + private MethodExceptions _methodExceptions; + @SerializedName(FIELD_HTTP_ERRORS) + private HTTPErrors _httpErrors; + @SerializedName(FIELD_HTTP_LATENCIES) + private HTTPLatencies _httpLatencies; + @SerializedName(FIELD_TOKEN_REFRESHES) + private long _tokenRefreshes; + @SerializedName(FIELD_AUTH_REJECTIONS) + private long _authRejections; + @SerializedName(FIELD_IMPRESSIONS_QUEUED) + private long _impressionsQueued; + @SerializedName(FIELD_IMPRESSIONS_DEDUPED) + private long _impressionsDeduped; + @SerializedName(FIELD_IMPRESSIONS_DROPPED) + private long _impressionsDropped; + @SerializedName(FIELD_SPLITS) + private long _splitCount; + @SerializedName(FIELD_SEGMENTS) + private long _segmentCount; + @SerializedName(FIELD_SEGMENTS_KEY) + private long _segmentKeyCount; + @SerializedName(FIELD_SESSION_LENGHT) + private long _sessionLengthMs; + @SerializedName(FIELD_EVENTS_QUEUED) + private long _eventsQueued; + @SerializedName(FIELD_EVENTS_DROPPED) + private long _eventsDropped; + @SerializedName(FIELD_STREAMING_EVENT) + private List _streamingEvents; + @SerializedName(FIELD_TAGS) + private List _tags; + + public LastSynchronization get_lastSynchronization() { + return _lastSynchronization; + } + + public void set_lastSynchronization(LastSynchronization _lastSynchronization) { + this._lastSynchronization = _lastSynchronization; + } + + public MethodLatencies get_methodLatencies() { + return _methodLatencies; + } + + public void set_methodLatencies(MethodLatencies _methodLatencies) { + this._methodLatencies = _methodLatencies; + } + + public MethodExceptions get_methodExceptions() { + return _methodExceptions; + } + + public void set_methodExceptions(MethodExceptions _methodExceptions) { + this._methodExceptions = _methodExceptions; + } + + public HTTPErrors get_httpErrors() { + return _httpErrors; + } + + public void set_httpErrors(HTTPErrors _httpErrors) { + this._httpErrors = _httpErrors; + } + + public HTTPLatencies get_httpLatencies() { + return _httpLatencies; + } + + public void set_httpLatencies(HTTPLatencies _httpLatencies) { + this._httpLatencies = _httpLatencies; + } + + public long get_tokenRefreshes() { + return _tokenRefreshes; + } + + public void set_tokenRefreshes(long _tokenRefreshes) { + this._tokenRefreshes = _tokenRefreshes; + } + + public long get_authRejections() { + return _authRejections; + } + + public void set_authRejections(long _authRejections) { + this._authRejections = _authRejections; + } + + public long get_impressionsQueued() { + return _impressionsQueued; + } + + public void set_impressionsQueued(long _impressionsQueued) { + this._impressionsQueued = _impressionsQueued; + } + + public long get_impressionsDeduped() { + return _impressionsDeduped; + } + + public void set_impressionsDeduped(long _impressionsDeduped) { + this._impressionsDeduped = _impressionsDeduped; + } + + public long get_impressionsDropped() { + return _impressionsDropped; + } + + public void set_impressionsDropped(long _impressionsDropped) { + this._impressionsDropped = _impressionsDropped; + } + + public long get_splitCount() { + return _splitCount; + } + + public void set_splitCount(long _splitCount) { + this._splitCount = _splitCount; + } + + public long get_segmentCount() { + return _segmentCount; + } + + public void set_segmentCount(long _segmentCount) { + this._segmentCount = _segmentCount; + } + + public long get_segmentKeyCount() { + return _segmentKeyCount; + } + + public void set_segmentKeyCount(long _segmentKeyCount) { + this._segmentKeyCount = _segmentKeyCount; + } + + public long get_sessionLengthMs() { + return _sessionLengthMs; + } + + public void set_sessionLengthMs(long _sessionLengthMs) { + this._sessionLengthMs = _sessionLengthMs; + } + + public long get_eventsQueued() { + return _eventsQueued; + } + + public void set_eventsQueued(long _eventsQueued) { + this._eventsQueued = _eventsQueued; + } + + public long get_eventsDropped() { + return _eventsDropped; + } + + public void set_eventsDropped(long _eventsDropped) { + this._eventsDropped = _eventsDropped; + } + + public List get_streamingEvents() { + return _streamingEvents; + } + + public void set_streamingEvents(List _streamingEvents) { + this._streamingEvents = _streamingEvents; + } + + public List get_tags() { + return _tags; + } + + public void set_tags(List _tags) { + this._tags = _tags; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/StreamingEvent.java b/client/src/main/java/io/split/telemetry/domain/StreamingEvent.java new file mode 100644 index 000000000..161b29762 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/StreamingEvent.java @@ -0,0 +1,46 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +public class StreamingEvent { + /* package private */ static final String FIELD_TYPE = "e"; + /* package private */ static final String FIELD_DATA = "d"; + /* package private */ static final String FIELD_TIMESTAMP = "t"; + + @SerializedName(FIELD_TYPE) + private int _type; + @SerializedName(FIELD_DATA) + private long _data; + @SerializedName(FIELD_TIMESTAMP) + private long _timestamp; + + public StreamingEvent(int _type, long _data, long _timestamp) { + this._type = _type; + this._data = _data; + this._timestamp = _timestamp; + } + + public int get_type() { + return _type; + } + + public void set_type(int _type) { + this._type = _type; + } + + public long get_data() { + return _data; + } + + public void set_data(long _data) { + this._data = _data; + } + + public long getTimestamp() { + return _timestamp; + } + + public void setTimestamp(long timestamp) { + this._timestamp = timestamp; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/URLOverrides.java b/client/src/main/java/io/split/telemetry/domain/URLOverrides.java new file mode 100644 index 000000000..5813f1d6c --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/URLOverrides.java @@ -0,0 +1,62 @@ +package io.split.telemetry.domain; + +import com.google.gson.annotations.SerializedName; + +public class URLOverrides { + /* package private */ static final String FIELD_SDK = "s"; + /* package private */ static final String FIELD_EVENTS = "e"; + /* package private */ static final String FIELD_AUTH = "a"; + /* package private */ static final String FIELD_STREAM = "st"; + /* package private */ static final String FIELD_TELEMETRY = "t"; + + @SerializedName(FIELD_SDK) + private boolean _sdk; + @SerializedName(FIELD_EVENTS) + private boolean _events; + @SerializedName(FIELD_AUTH) + private boolean _auth; + @SerializedName(FIELD_STREAM) + private boolean _stream; + @SerializedName(FIELD_TELEMETRY) + private boolean _telemetry; + + public boolean is_sdk() { + return _sdk; + } + + public void set_sdk(boolean _sdk) { + this._sdk = _sdk; + } + + public boolean is_events() { + return _events; + } + + public void set_events(boolean _events) { + this._events = _events; + } + + public boolean is_auth() { + return _auth; + } + + public void set_auth(boolean _auth) { + this._auth = _auth; + } + + public boolean is_stream() { + return _stream; + } + + public void set_stream(boolean _stream) { + this._stream = _stream; + } + + public boolean is_telemetry() { + return _telemetry; + } + + public void set_telemetry(boolean _telemetry) { + this._telemetry = _telemetry; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/EventsDataRecordsEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/EventsDataRecordsEnum.java new file mode 100644 index 000000000..8beb3ac48 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/EventsDataRecordsEnum.java @@ -0,0 +1,6 @@ +package io.split.telemetry.domain.enums; + +public enum EventsDataRecordsEnum { + EVENTS_QUEUED, + EVENTS_DROPPED +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/FactoryCountersEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/FactoryCountersEnum.java new file mode 100644 index 000000000..387084612 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/FactoryCountersEnum.java @@ -0,0 +1,6 @@ +package io.split.telemetry.domain.enums; + +public enum FactoryCountersEnum { + BUR_TIMEOUTS, + NON_READY_USAGES +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/HTTPLatenciesEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/HTTPLatenciesEnum.java new file mode 100644 index 000000000..6f858397b --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/HTTPLatenciesEnum.java @@ -0,0 +1,11 @@ +package io.split.telemetry.domain.enums; + +public enum HTTPLatenciesEnum { + SPLITS, + SEGMENTS, + IMPRESSIONS, + IMPRESSIONS_COUNT, + EVENTS, + TELEMETRY, + TOKEN +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/ImpressionsDataTypeEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/ImpressionsDataTypeEnum.java new file mode 100644 index 000000000..3ca134c93 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/ImpressionsDataTypeEnum.java @@ -0,0 +1,7 @@ +package io.split.telemetry.domain.enums; + +public enum ImpressionsDataTypeEnum { + IMPRESSIONS_QUEUED, + IMPRESSIONS_DROPPED, + IMPRESSIONS_DEDUPED +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/LastSynchronizationRecordsEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/LastSynchronizationRecordsEnum.java new file mode 100644 index 000000000..d2439395b --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/LastSynchronizationRecordsEnum.java @@ -0,0 +1,11 @@ +package io.split.telemetry.domain.enums; + +public enum LastSynchronizationRecordsEnum { + SPLITS, + SEGMENTS, + IMPRESSIONS, + IMPRESSIONS_COUNT, + EVENTS, + TOKEN, + TELEMETRY +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/MethodEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/MethodEnum.java new file mode 100644 index 000000000..8f99527f2 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/MethodEnum.java @@ -0,0 +1,19 @@ +package io.split.telemetry.domain.enums; + +public enum MethodEnum { + TREATMENT("getTreatment"), + TREATMENTS("getTreatments"), + TREATMENT_WITH_CONFIG("getTreatmentWithConfig"), + TREATMENTS_WITH_CONFIG("getTreatmentsWithConfig"), + TRACK("track"); + + private String _method; + + MethodEnum(String method) { + _method = method; + } + + public String getMethod() { + return _method; + } +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/PushCountersEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/PushCountersEnum.java new file mode 100644 index 000000000..53f023f74 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/PushCountersEnum.java @@ -0,0 +1,6 @@ +package io.split.telemetry.domain.enums; + +public enum PushCountersEnum { + AUTH_REJECTIONS, + TOKEN_REFRESHES +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/ResourceEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/ResourceEnum.java new file mode 100644 index 000000000..04af11162 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/ResourceEnum.java @@ -0,0 +1,11 @@ +package io.split.telemetry.domain.enums; + +public enum ResourceEnum { + SPLIT_SYNC, + SEGMENT_SYNC, + IMPRESSION_SYNC, + IMPRESSION_COUNT_SYNC, + EVENT_SYNC, + TELEMETRY_SYNC, + TOKEN_SYNC +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/SdkRecordsEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/SdkRecordsEnum.java new file mode 100644 index 000000000..6f8780811 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/SdkRecordsEnum.java @@ -0,0 +1,5 @@ +package io.split.telemetry.domain.enums; + +public enum SdkRecordsEnum { + SESSION +} diff --git a/client/src/main/java/io/split/telemetry/domain/enums/StreamEventsEnum.java b/client/src/main/java/io/split/telemetry/domain/enums/StreamEventsEnum.java new file mode 100644 index 000000000..30f977c6f --- /dev/null +++ b/client/src/main/java/io/split/telemetry/domain/enums/StreamEventsEnum.java @@ -0,0 +1,69 @@ +package io.split.telemetry.domain.enums; + +public enum StreamEventsEnum { + CONNECTION_ESTABLISHED(0), + OCCUPANCY_PRI(10), + OCCUPANCY_SEC(20), + STREAMING_STATUS(30), + SSE_CONNECTION_ERROR(40), + TOKEN_REFRESH(50), + ABLY_ERROR(60), + SYNC_MODE_UPDATE(70); + + + private int _type; + + StreamEventsEnum(int type) { + _type = type; + } + + public int getType() { + return _type; + } + + public enum StreamingStatusValues { + STREAMING_DISABLED(0), + STREAMING_PAUSED(2), + STREAMING_ENABLED(1); + + private long _value; + + StreamingStatusValues(long value) { + _value = value; + } + + public long getValue() { + return _value; + } + } + + public enum SseConnectionErrorValues { + REQUESTED_CONNECTION_ERROR(0), + NON_REQUESTED_CONNECTION_ERROR (1); + + private long _value; + + SseConnectionErrorValues(long value) { + _value = value; + } + + public long getValue() { + return _value; + } + } + + public enum SyncModeUpdateValues { + STREAMING_EVENT(0), + POLLING_EVENT(1); + + private long _value; + + SyncModeUpdateValues(long value) { + _value = value; + } + + public long getValue() { + return _value; + } + } +} diff --git a/client/src/main/java/io/split/telemetry/storage/InMemoryTelemetryStorage.java b/client/src/main/java/io/split/telemetry/storage/InMemoryTelemetryStorage.java new file mode 100644 index 000000000..644f43eb5 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/InMemoryTelemetryStorage.java @@ -0,0 +1,350 @@ +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.utils.AtomicLongArray; +import io.split.telemetry.utils.BucketCalculator; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +public class InMemoryTelemetryStorage implements TelemetryStorage{ + public static final int MAX_LATENCY_BUCKET_COUNT = 23; + + //Latencies + private final ConcurrentMap _methodLatencies = Maps.newConcurrentMap(); + private final ConcurrentMap _httpLatencies = Maps.newConcurrentMap(); + + //Counters + private final ConcurrentMap _exceptionsCounters = Maps.newConcurrentMap(); + private final ConcurrentMap _pushCounters = Maps.newConcurrentMap(); + private final ConcurrentMap _factoryCounters = Maps.newConcurrentMap(); + + //Records + private final ConcurrentMap _impressionsDataRecords = Maps.newConcurrentMap(); + private final ConcurrentMap _eventsDataRecords = Maps.newConcurrentMap(); + private final ConcurrentMap _lastSynchronizationRecords = Maps.newConcurrentMap(); + private final ConcurrentMap _sdkRecords = Maps.newConcurrentMap(); + + //HTTPErrors + private final ConcurrentMap> _httpErrors = Maps.newConcurrentMap(); + + //StreamingEvents + private final Object _streamingEventsLock = new Object(); + private final List _streamingEvents = new ArrayList<>(); + + //Tags + private final Object _tagsLock = new Object(); + private final List _tags = new ArrayList<>(); + + public InMemoryTelemetryStorage() { + initMethodLatencies(); + initHttpLatencies(); + initHttpErrors(); + initMethodExceptions(); + initFactoryCounters(); + initImpressionDataCounters(); + initPushCounters(); + initSdkRecords(); + initLastSynchronizationRecords(); + initEventDataRecords(); + } + + @Override + public long getBURTimeouts() { + return _factoryCounters.get(FactoryCountersEnum.BUR_TIMEOUTS).get(); + } + + @Override + public long getNonReadyUsages() { + return _factoryCounters.get(FactoryCountersEnum.NON_READY_USAGES).get(); + } + + @Override + public MethodExceptions popExceptions() { + MethodExceptions exceptions = new MethodExceptions(); + exceptions.set_treatment(_exceptionsCounters.get(MethodEnum.TREATMENT).get()); + exceptions.set_treatments(_exceptionsCounters.get(MethodEnum.TREATMENTS).get()); + exceptions.set_treatmentWithConfig(_exceptionsCounters.get(MethodEnum.TREATMENT_WITH_CONFIG).get()); + exceptions.set_treatmentsWithConfig(_exceptionsCounters.get(MethodEnum.TREATMENTS_WITH_CONFIG).get()); + exceptions.set_track(_exceptionsCounters.get(MethodEnum.TRACK).get()); + + _exceptionsCounters.clear(); + initMethodExceptions(); + + return exceptions; + } + + @Override + public MethodLatencies popLatencies() { + MethodLatencies latencies = new MethodLatencies(); + latencies.set_treatment(_methodLatencies.get(MethodEnum.TREATMENT).fetchAndClearAll()); + latencies.set_treatments(_methodLatencies.get(MethodEnum.TREATMENTS).fetchAndClearAll()); + latencies.set_treatmentWithConfig(_methodLatencies.get(MethodEnum.TREATMENT_WITH_CONFIG).fetchAndClearAll()); + latencies.set_treatmentsWithConfig(_methodLatencies.get(MethodEnum.TREATMENTS_WITH_CONFIG).fetchAndClearAll()); + latencies.set_track(_methodLatencies.get(MethodEnum.TRACK).fetchAndClearAll()); + + _methodLatencies.clear(); + initMethodLatencies(); + + return latencies; + } + + @Override + public void recordNonReadyUsage() { + _factoryCounters.get(FactoryCountersEnum.NON_READY_USAGES).incrementAndGet(); + } + + @Override + public void recordBURTimeout() { + _factoryCounters.get(FactoryCountersEnum.BUR_TIMEOUTS).incrementAndGet(); + } + + + @Override + public void recordLatency(MethodEnum method, long latency) { + int bucket = BucketCalculator.getBucketForLatency(latency); + _methodLatencies.get(method).increment(bucket); + } + + @Override + public void recordException(MethodEnum method) { + _exceptionsCounters.get(method).incrementAndGet(); + } + + @Override + public long getImpressionsStats(ImpressionsDataTypeEnum dataType) { + return _impressionsDataRecords.get(dataType).get(); + } + + @Override + public long getEventStats(EventsDataRecordsEnum dataType) { + return _eventsDataRecords.get(dataType).get(); + } + + @Override + public LastSynchronization getLastSynchronization() { + LastSynchronization lastSynchronization = new LastSynchronization(); + lastSynchronization.set_splits(_lastSynchronizationRecords.get(LastSynchronizationRecordsEnum.SPLITS).get()); + lastSynchronization.set_segments(_lastSynchronizationRecords.get(LastSynchronizationRecordsEnum.SEGMENTS).get()); + lastSynchronization.set_impressions(_lastSynchronizationRecords.get(LastSynchronizationRecordsEnum.IMPRESSIONS).get()); + lastSynchronization.set_impressionsCount(_lastSynchronizationRecords.get(LastSynchronizationRecordsEnum.IMPRESSIONS_COUNT).get()); + lastSynchronization.set_events(_lastSynchronizationRecords.get(LastSynchronizationRecordsEnum.EVENTS).get()); + lastSynchronization.set_telemetry(_lastSynchronizationRecords.get(LastSynchronizationRecordsEnum.TELEMETRY).get()); + lastSynchronization.set_token(_lastSynchronizationRecords.get(LastSynchronizationRecordsEnum.TOKEN).get()); + + return lastSynchronization; + } + + @Override + public HTTPErrors popHTTPErrors() { + HTTPErrors errors = new HTTPErrors(); + errors.set_splits(_httpErrors.get(ResourceEnum.SPLIT_SYNC)); + errors.set_segments(_httpErrors.get(ResourceEnum.SEGMENT_SYNC)); + errors.set_impressions(_httpErrors.get(ResourceEnum.IMPRESSION_SYNC)); + errors.set_impressionsCount(_httpErrors.get(ResourceEnum.IMPRESSION_COUNT_SYNC)); + errors.set_events(_httpErrors.get(ResourceEnum.EVENT_SYNC)); + errors.set_telemetry(_httpErrors.get(ResourceEnum.TELEMETRY_SYNC)); + errors.set_token(_httpErrors.get(ResourceEnum.TOKEN_SYNC)); + + _httpErrors.clear(); + initHttpErrors(); + + return errors; + } + + @Override + public HTTPLatencies popHTTPLatencies(){ + HTTPLatencies latencies = new HTTPLatencies(); + latencies.set_splits(_httpLatencies.get(HTTPLatenciesEnum.SPLITS).fetchAndClearAll()); + latencies.set_segments(_httpLatencies.get(HTTPLatenciesEnum.SEGMENTS).fetchAndClearAll()); + latencies.set_impressions(_httpLatencies.get(HTTPLatenciesEnum.IMPRESSIONS).fetchAndClearAll()); + latencies.set_impressionsCount(_httpLatencies.get(HTTPLatenciesEnum.IMPRESSIONS_COUNT).fetchAndClearAll()); + latencies.set_events(_httpLatencies.get(HTTPLatenciesEnum.EVENTS).fetchAndClearAll()); + latencies.set_telemetry(_httpLatencies.get(HTTPLatenciesEnum.TELEMETRY).fetchAndClearAll()); + latencies.set_token(_httpLatencies.get(HTTPLatenciesEnum.TOKEN).fetchAndClearAll()); + + _httpLatencies.clear(); + initHttpLatencies(); + + return latencies; + } + + @Override + public long popAuthRejections() { + long authRejections = _pushCounters.get(PushCountersEnum.AUTH_REJECTIONS).get(); + + _pushCounters.replace(PushCountersEnum.AUTH_REJECTIONS, new AtomicLong()); + + return authRejections; + } + + @Override + public long popTokenRefreshes() { + long tokenRefreshes = _pushCounters.get(PushCountersEnum.TOKEN_REFRESHES).get(); + + _pushCounters.replace(PushCountersEnum.TOKEN_REFRESHES, new AtomicLong()); + + return tokenRefreshes; + } + + @Override + public List popStreamingEvents() { + synchronized (_streamingEventsLock) { + List streamingEvents = _streamingEvents.stream().collect(Collectors.toList()); + + _streamingEvents.clear(); + + return streamingEvents; + } + } + + @Override + public List popTags() { + synchronized (_tagsLock) { + List tags = _tags.stream().collect(Collectors.toList()); + + _tags.clear(); + + return tags; + } + } + + @Override + public long getSessionLength() { + return _sdkRecords.get(SdkRecordsEnum.SESSION).get(); + } + + @Override + public void addTag(String tag) { + synchronized (_tagsLock) { + _tags.add(tag); + } + } + + @Override + public void recordImpressionStats(ImpressionsDataTypeEnum dataType, long count) { + _impressionsDataRecords.get(dataType).addAndGet(count); + } + + @Override + public void recordEventStats(EventsDataRecordsEnum dataType, long count) { + _eventsDataRecords.get(dataType).addAndGet(count); + } + + @Override + public void recordSuccessfulSync(LastSynchronizationRecordsEnum resource, long time) { + _lastSynchronizationRecords.replace(resource, new AtomicLong(time)); + } + + @Override + public void recordSyncError(ResourceEnum resource, int status) { + ConcurrentMap errors = _httpErrors.get(resource); + errors.putIfAbsent(Long.valueOf(status), 0l); + errors.replace(Long.valueOf(status), errors.get(Long.valueOf(status)) + 1); + } + + @Override + public void recordSyncLatency(HTTPLatenciesEnum resource, long latency) { + int bucket = BucketCalculator.getBucketForLatency(latency); + _httpLatencies.get(resource).increment(bucket); + + } + + @Override + public void recordAuthRejections() { + _pushCounters.get(PushCountersEnum.AUTH_REJECTIONS).incrementAndGet(); + } + + @Override + public void recordTokenRefreshes() { + _pushCounters.get(PushCountersEnum.TOKEN_REFRESHES).incrementAndGet(); + } + + @Override + public void recordStreamingEvents(StreamingEvent streamingEvent) { + synchronized (_streamingEventsLock) { + _streamingEvents.add(streamingEvent); + } + } + + @Override + public void recordSessionLength(long sessionLength) { + _sdkRecords.replace(SdkRecordsEnum.SESSION, new AtomicLong(sessionLength)); + } + + private void initMethodLatencies() { + _methodLatencies.put(MethodEnum.TREATMENT, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _methodLatencies.put(MethodEnum.TREATMENTS, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _methodLatencies.put(MethodEnum.TREATMENT_WITH_CONFIG, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _methodLatencies.put(MethodEnum.TREATMENTS_WITH_CONFIG, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _methodLatencies.put(MethodEnum.TRACK, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + } + + private void initHttpLatencies() { + _httpLatencies.put(HTTPLatenciesEnum.SPLITS, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _httpLatencies.put(HTTPLatenciesEnum.SEGMENTS, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _httpLatencies.put(HTTPLatenciesEnum.IMPRESSIONS, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _httpLatencies.put(HTTPLatenciesEnum.IMPRESSIONS_COUNT, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _httpLatencies.put(HTTPLatenciesEnum.EVENTS, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _httpLatencies.put(HTTPLatenciesEnum.TELEMETRY, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + _httpLatencies.put(HTTPLatenciesEnum.TOKEN, new AtomicLongArray(MAX_LATENCY_BUCKET_COUNT)); + } + + private void initHttpErrors() { + _httpErrors.put(ResourceEnum.SPLIT_SYNC, Maps.newConcurrentMap()); + _httpErrors.put(ResourceEnum.SEGMENT_SYNC, Maps.newConcurrentMap()); + _httpErrors.put(ResourceEnum.IMPRESSION_SYNC, Maps.newConcurrentMap()); + _httpErrors.put(ResourceEnum.IMPRESSION_COUNT_SYNC, Maps.newConcurrentMap()); + _httpErrors.put(ResourceEnum.EVENT_SYNC, Maps.newConcurrentMap()); + _httpErrors.put(ResourceEnum.TELEMETRY_SYNC, Maps.newConcurrentMap()); + _httpErrors.put(ResourceEnum.TOKEN_SYNC, Maps.newConcurrentMap()); + } + + private void initMethodExceptions() { + _exceptionsCounters.put(MethodEnum.TREATMENT, new AtomicLong()); + _exceptionsCounters.put(MethodEnum.TREATMENTS, new AtomicLong()); + _exceptionsCounters.put(MethodEnum.TREATMENT_WITH_CONFIG, new AtomicLong()); + _exceptionsCounters.put(MethodEnum.TREATMENTS_WITH_CONFIG, new AtomicLong()); + _exceptionsCounters.put(MethodEnum.TRACK, new AtomicLong()); + } + + private void initFactoryCounters() { + _factoryCounters.put(FactoryCountersEnum.BUR_TIMEOUTS, new AtomicLong()); + _factoryCounters.put(FactoryCountersEnum.NON_READY_USAGES, new AtomicLong()); + } + + private void initImpressionDataCounters() { + _impressionsDataRecords.put(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, new AtomicLong()); + _impressionsDataRecords.put(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, new AtomicLong()); + _impressionsDataRecords.put(ImpressionsDataTypeEnum.IMPRESSIONS_QUEUED, new AtomicLong()); + } + + private void initPushCounters() { + _pushCounters.put(PushCountersEnum.AUTH_REJECTIONS, new AtomicLong()); + _pushCounters.put(PushCountersEnum.TOKEN_REFRESHES, new AtomicLong()); + } + + private void initSdkRecords() { + _sdkRecords.put(SdkRecordsEnum.SESSION, new AtomicLong()); + } + + private void initLastSynchronizationRecords() { + _lastSynchronizationRecords.put(LastSynchronizationRecordsEnum.SPLITS, new AtomicLong()); + _lastSynchronizationRecords.put(LastSynchronizationRecordsEnum.SEGMENTS, new AtomicLong()); + _lastSynchronizationRecords.put(LastSynchronizationRecordsEnum.EVENTS, new AtomicLong()); + _lastSynchronizationRecords.put(LastSynchronizationRecordsEnum.IMPRESSIONS, new AtomicLong()); + _lastSynchronizationRecords.put(LastSynchronizationRecordsEnum.IMPRESSIONS_COUNT, new AtomicLong()); + _lastSynchronizationRecords.put(LastSynchronizationRecordsEnum.TOKEN, new AtomicLong()); + _lastSynchronizationRecords.put(LastSynchronizationRecordsEnum.TELEMETRY, new AtomicLong()); + } + + private void initEventDataRecords() { + _eventsDataRecords.put(EventsDataRecordsEnum.EVENTS_DROPPED, new AtomicLong()); + _eventsDataRecords.put(EventsDataRecordsEnum.EVENTS_QUEUED, new AtomicLong()); + } +} diff --git a/client/src/main/java/io/split/telemetry/storage/NoopTelemetryStorage.java b/client/src/main/java/io/split/telemetry/storage/NoopTelemetryStorage.java new file mode 100644 index 000000000..3673d0c0a --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/NoopTelemetryStorage.java @@ -0,0 +1,149 @@ +package io.split.telemetry.storage; + +import io.split.telemetry.domain.*; +import io.split.telemetry.domain.enums.*; + +import java.util.List; + +public class NoopTelemetryStorage implements TelemetryStorage{ + + @Override + public void recordNonReadyUsage() { + + } + + @Override + public void recordBURTimeout() { + + } + + @Override + public void recordLatency(MethodEnum method, long latency) { + + } + + @Override + public void recordException(MethodEnum method) { + + } + + @Override + public void addTag(String tag) { + + } + + @Override + public void recordImpressionStats(ImpressionsDataTypeEnum dataType, long count) { + + } + + @Override + public void recordEventStats(EventsDataRecordsEnum dataType, long count) { + + } + + @Override + public void recordSuccessfulSync(LastSynchronizationRecordsEnum resource, long time) { + + } + + @Override + public void recordSyncError(ResourceEnum resource, int status) { + + } + + @Override + public void recordSyncLatency(HTTPLatenciesEnum resource, long latency) { + + } + + @Override + public void recordAuthRejections() { + + } + + @Override + public void recordTokenRefreshes() { + + } + + @Override + public void recordStreamingEvents(StreamingEvent streamingEvent) { + + } + + @Override + public void recordSessionLength(long sessionLength) { + + } + + @Override + public long getBURTimeouts() { + return 0; + } + + @Override + public long getNonReadyUsages() { + return 0; + } + + @Override + public MethodExceptions popExceptions() throws Exception { + return null; + } + + @Override + public MethodLatencies popLatencies() throws Exception { + return null; + } + + @Override + public long getImpressionsStats(ImpressionsDataTypeEnum data) { + return 0; + } + + @Override + public long getEventStats(EventsDataRecordsEnum type) { + return 0; + } + + @Override + public LastSynchronization getLastSynchronization() { + return null; + } + + @Override + public HTTPErrors popHTTPErrors() { + return null; + } + + @Override + public HTTPLatencies popHTTPLatencies(){ + return null; + } + + @Override + public long popAuthRejections() { + return 0; + } + + @Override + public long popTokenRefreshes() { + return 0; + } + + @Override + public List popStreamingEvents() { + return null; + } + + @Override + public List popTags() { + return null; + } + + @Override + public long getSessionLength() { + return 0; + } +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryConfigConsumer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryConfigConsumer.java new file mode 100644 index 000000000..680d128b8 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryConfigConsumer.java @@ -0,0 +1,6 @@ +package io.split.telemetry.storage; + +public interface TelemetryConfigConsumer { + long getBURTimeouts(); + long getNonReadyUsages(); +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryConfigProducer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryConfigProducer.java new file mode 100644 index 000000000..64348c154 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryConfigProducer.java @@ -0,0 +1,6 @@ +package io.split.telemetry.storage; + +public interface TelemetryConfigProducer { + void recordNonReadyUsage(); + void recordBURTimeout(); +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryEvaluationConsumer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryEvaluationConsumer.java new file mode 100644 index 000000000..ca63112ac --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryEvaluationConsumer.java @@ -0,0 +1,9 @@ +package io.split.telemetry.storage; + +import io.split.telemetry.domain.MethodExceptions; +import io.split.telemetry.domain.MethodLatencies; + +public interface TelemetryEvaluationConsumer { + MethodExceptions popExceptions() throws Exception; + MethodLatencies popLatencies() throws Exception; +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryEvaluationProducer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryEvaluationProducer.java new file mode 100644 index 000000000..005106c30 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryEvaluationProducer.java @@ -0,0 +1,8 @@ +package io.split.telemetry.storage; + +import io.split.telemetry.domain.enums.MethodEnum; + +public interface TelemetryEvaluationProducer { + void recordLatency(MethodEnum method, long latency); + void recordException(MethodEnum method); +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeConsumer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeConsumer.java new file mode 100644 index 000000000..6a746e783 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeConsumer.java @@ -0,0 +1,23 @@ +package io.split.telemetry.storage; + +import io.split.telemetry.domain.HTTPErrors; +import io.split.telemetry.domain.HTTPLatencies; +import io.split.telemetry.domain.LastSynchronization; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; + +import java.util.List; + +public interface TelemetryRuntimeConsumer { + long getImpressionsStats(ImpressionsDataTypeEnum data); + long getEventStats(EventsDataRecordsEnum type); + LastSynchronization getLastSynchronization(); + HTTPErrors popHTTPErrors(); + HTTPLatencies popHTTPLatencies(); + long popAuthRejections(); + long popTokenRefreshes(); + List popStreamingEvents(); + List popTags(); + long getSessionLength(); +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeProducer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeProducer.java new file mode 100644 index 000000000..2baf016f0 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryRuntimeProducer.java @@ -0,0 +1,17 @@ +package io.split.telemetry.storage; + +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.enums.*; + +public interface TelemetryRuntimeProducer { + void addTag(String tag); + void recordImpressionStats(ImpressionsDataTypeEnum dataType, long count); + void recordEventStats(EventsDataRecordsEnum dataType, long count); + void recordSuccessfulSync(LastSynchronizationRecordsEnum resource, long time); + void recordSyncError(ResourceEnum resource, int status); + void recordSyncLatency(HTTPLatenciesEnum resource, long latency); + void recordAuthRejections(); + void recordTokenRefreshes(); + void recordStreamingEvents(StreamingEvent streamingEvent); + void recordSessionLength(long sessionLength); +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryStorage.java b/client/src/main/java/io/split/telemetry/storage/TelemetryStorage.java new file mode 100644 index 000000000..477965099 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryStorage.java @@ -0,0 +1,4 @@ +package io.split.telemetry.storage; + +public interface TelemetryStorage extends TelemetryStorageConsumer, TelemetryStorageProducer{ +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryStorageConsumer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryStorageConsumer.java new file mode 100644 index 000000000..7efd537c5 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryStorageConsumer.java @@ -0,0 +1,4 @@ +package io.split.telemetry.storage; + +public interface TelemetryStorageConsumer extends TelemetryConfigConsumer, TelemetryRuntimeConsumer, TelemetryEvaluationConsumer{ +} diff --git a/client/src/main/java/io/split/telemetry/storage/TelemetryStorageProducer.java b/client/src/main/java/io/split/telemetry/storage/TelemetryStorageProducer.java new file mode 100644 index 000000000..c3ef23031 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/storage/TelemetryStorageProducer.java @@ -0,0 +1,4 @@ +package io.split.telemetry.storage; + +public interface TelemetryStorageProducer extends TelemetryEvaluationProducer, TelemetryConfigProducer, TelemetryRuntimeProducer{ +} diff --git a/client/src/main/java/io/split/telemetry/synchronizer/HttpTelemetryMemorySender.java b/client/src/main/java/io/split/telemetry/synchronizer/HttpTelemetryMemorySender.java new file mode 100644 index 000000000..48e5cbac1 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/synchronizer/HttpTelemetryMemorySender.java @@ -0,0 +1,51 @@ +package io.split.telemetry.synchronizer; + +import com.google.common.annotations.VisibleForTesting; +import io.split.client.utils.Utils; +import io.split.service.HttpPostImp; +import io.split.telemetry.domain.Config; +import io.split.telemetry.domain.Stats; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; + +import java.net.URI; +import java.net.URISyntaxException; + +public class HttpTelemetryMemorySender{ + + private static final String CONFIG_ENDPOINT_PATH = "metrics/config"; + private static final String STATS_ENDPOINT_PATH = "metrics/usage"; + private static final String CONFIG_METRICS = "Config metrics "; + private static final String STATS_METRICS = "Stats metrics "; + + private final URI _impressionConfigTarget; + private final URI _impressionStatsTarget; + private final HttpPostImp _httpPost; + + public static HttpTelemetryMemorySender create(CloseableHttpClient client, URI telemetryRootEndpoint, TelemetryRuntimeProducer telemetryRuntimeProducer) throws URISyntaxException { + return new HttpTelemetryMemorySender(client, + Utils.appendPath(telemetryRootEndpoint,CONFIG_ENDPOINT_PATH), + Utils.appendPath(telemetryRootEndpoint, STATS_ENDPOINT_PATH), + telemetryRuntimeProducer + ); + } + + @VisibleForTesting + HttpTelemetryMemorySender(CloseableHttpClient client, URI impressionConfigTarget, URI impressionStatsTarget, TelemetryRuntimeProducer telemetryRuntimeProducer) { + _httpPost = new HttpPostImp(client, telemetryRuntimeProducer); + _impressionConfigTarget = impressionConfigTarget; + _impressionStatsTarget = impressionStatsTarget; + } + + public void postConfig(Config config) { + _httpPost.post(_impressionConfigTarget, config, CONFIG_METRICS, HTTPLatenciesEnum.TELEMETRY, LastSynchronizationRecordsEnum.TELEMETRY, ResourceEnum.TELEMETRY_SYNC); + } + + public void postStats(Stats stats) { + _httpPost.post(_impressionStatsTarget, stats, STATS_METRICS, HTTPLatenciesEnum.TELEMETRY, LastSynchronizationRecordsEnum.TELEMETRY, ResourceEnum.TELEMETRY_SYNC); + } + +} diff --git a/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySubmitter.java b/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySubmitter.java new file mode 100644 index 000000000..2be82aa9a --- /dev/null +++ b/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySubmitter.java @@ -0,0 +1,160 @@ +package io.split.telemetry.synchronizer; + +import com.google.common.annotations.VisibleForTesting; +import io.split.cache.SegmentCache; +import io.split.cache.SplitCache; +import io.split.client.SplitClientConfig; +import io.split.client.impressions.ImpressionListener; +import io.split.client.impressions.ImpressionsManager; +import io.split.integrations.IntegrationsConfig; +import io.split.integrations.NewRelicListener; +import io.split.telemetry.domain.Config; +import io.split.telemetry.domain.Rates; +import io.split.telemetry.domain.Stats; +import io.split.telemetry.domain.URLOverrides; +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.telemetry.storage.TelemetryStorageConsumer; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class TelemetrySubmitter implements TelemetrySynchronizer{ + + private static final int OPERATION_MODE = 0; + private static final String STORAGE = "memory"; + + private HttpTelemetryMemorySender _httpHttpTelemetryMemorySender; + private TelemetryStorageConsumer _teleTelemetryStorageConsumer; + private SplitCache _splitCache; + private SegmentCache _segmentCache; + private final long _initStartTime; + + public TelemetrySubmitter(CloseableHttpClient client, URI telemetryRootEndpoint, TelemetryStorageConsumer telemetryStorageConsumer, SplitCache splitCache, + SegmentCache segmentCache, TelemetryRuntimeProducer telemetryRuntimeProducer, long initStartTime) throws URISyntaxException { + _httpHttpTelemetryMemorySender = HttpTelemetryMemorySender.create(client, telemetryRootEndpoint, telemetryRuntimeProducer); + _teleTelemetryStorageConsumer = telemetryStorageConsumer; + _splitCache = splitCache; + _segmentCache = segmentCache; + _initStartTime = initStartTime; + } + + @Override + public void synchronizeConfig(SplitClientConfig config, long readyTimeStamp, Map factoryInstances, List tags) { + _httpHttpTelemetryMemorySender.postConfig(generateConfig(config, readyTimeStamp, factoryInstances, tags)); + } + + @Override + public void synchronizeStats() throws Exception { + _httpHttpTelemetryMemorySender.postStats(generateStats()); + } + + @Override + public void finalSynchronization(long splitCount, long segmentCount, long segmentKeyCount) throws Exception { + Stats stats = generateStats(); + stats.set_splitCount(splitCount); + stats.set_segmentCount(segmentCount); + stats.set_segmentKeyCount(segmentKeyCount); + _httpHttpTelemetryMemorySender.postStats(stats); + } + + @VisibleForTesting + Stats generateStats() throws Exception { + Stats stats = new Stats(); + stats.set_lastSynchronization(_teleTelemetryStorageConsumer.getLastSynchronization()); + stats.set_methodLatencies(_teleTelemetryStorageConsumer.popLatencies()); + stats.set_methodExceptions(_teleTelemetryStorageConsumer.popExceptions()); + stats.set_httpErrors(_teleTelemetryStorageConsumer.popHTTPErrors()); + stats.set_httpLatencies(_teleTelemetryStorageConsumer.popHTTPLatencies()); + stats.set_tokenRefreshes(_teleTelemetryStorageConsumer.popTokenRefreshes()); + stats.set_authRejections(_teleTelemetryStorageConsumer.popAuthRejections()); + stats.set_impressionsQueued(_teleTelemetryStorageConsumer.getImpressionsStats(ImpressionsDataTypeEnum.IMPRESSIONS_QUEUED)); + stats.set_impressionsDeduped(_teleTelemetryStorageConsumer.getImpressionsStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED)); + stats.set_impressionsDropped(_teleTelemetryStorageConsumer.getImpressionsStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED)); + stats.set_splitCount(_splitCache.getAll().stream().count()); + stats.set_segmentCount(_segmentCache.getAll().stream().count()); + stats.set_segmentKeyCount(_segmentCache.getKeyCount()); + stats.set_sessionLengthMs(_teleTelemetryStorageConsumer.getSessionLength()); + stats.set_eventsQueued(_teleTelemetryStorageConsumer.getEventStats(EventsDataRecordsEnum.EVENTS_QUEUED)); + stats.set_eventsDropped(_teleTelemetryStorageConsumer.getEventStats(EventsDataRecordsEnum.EVENTS_DROPPED)); + stats.set_streamingEvents(_teleTelemetryStorageConsumer.popStreamingEvents()); + stats.set_tags(_teleTelemetryStorageConsumer.popTags()); + return stats; + } + + @VisibleForTesting + Config generateConfig(SplitClientConfig splitClientConfig, long readyTimestamp, Map factoryInstances, List tags) { + Config config = new Config(); + Rates rates = new Rates(); + URLOverrides urlOverrides = new URLOverrides(); + List impressionsListeners = new ArrayList<>(); + if(splitClientConfig.integrationsConfig() != null) { + impressionsListeners.addAll(splitClientConfig.integrationsConfig().getImpressionsListeners(IntegrationsConfig.Execution.ASYNC)); + impressionsListeners.addAll(splitClientConfig.integrationsConfig().getImpressionsListeners(IntegrationsConfig.Execution.SYNC)); + } + List impressions = getImpressions(impressionsListeners); + + rates.set_telemetry(splitClientConfig.get_telemetryRefreshRate()); + rates.set_events(splitClientConfig.eventFlushIntervalInMillis()); + rates.set_impressions(splitClientConfig.impressionsRefreshRate()); + rates.set_segments(splitClientConfig.segmentsRefreshRate()); + rates.set_splits(splitClientConfig.featuresRefreshRate()); + + urlOverrides.set_auth(!SplitClientConfig.AUTH_ENDPOINT.equals(splitClientConfig.authServiceURL())); + urlOverrides.set_stream(!SplitClientConfig.STREAMING_ENDPOINT.equals(splitClientConfig.streamingServiceURL())); + urlOverrides.set_sdk(!SplitClientConfig.SDK_ENDPOINT.equals(splitClientConfig.endpoint())); + urlOverrides.set_events(!SplitClientConfig.EVENTS_ENDPOINT.equals(splitClientConfig.eventsEndpoint())); + urlOverrides.set_telemetry(!SplitClientConfig.TELEMETRY_ENDPOINT.equals(splitClientConfig.telemetryURL())); + + config.set_burTimeouts(_teleTelemetryStorageConsumer.getBURTimeouts()); + config.set_nonReadyUsages(_teleTelemetryStorageConsumer.getNonReadyUsages()); + config.set_httpProxyDetected(splitClientConfig.proxy() != null); + config.set_impressionsMode(getImpressionsMode(splitClientConfig)); + config.set_integrations(impressions); + config.set_impressionsListenerEnabled((impressionsListeners.size()-impressions.size()) > 0); + config.set_operationMode(OPERATION_MODE); + config.set_storage(STORAGE); + config.set_impressionsQueueSize(splitClientConfig.impressionsQueueSize()); + config.set_redundantFactories(getRedundantFactories(factoryInstances)); + config.set_eventsQueueSize(splitClientConfig.eventsQueueSize()); + config.set_tags(getListMaxSize(tags)); + config.set_activeFactories(factoryInstances.size()); + config.set_timeUntilReady(readyTimestamp - _initStartTime); + config.set_rates(rates); + config.set_urlOverrides(urlOverrides); + config.set_streamingEnabled(splitClientConfig.streamingEnabled()); + return config; + } + + private long getRedundantFactories(Map factoryInstances) { + long count = 0; + for(Long l :factoryInstances.values()) { + count = count + l - 1l; + } + return count; + } + + private int getImpressionsMode(SplitClientConfig config) { + return ImpressionsManager.Mode.OPTIMIZED.equals(config.impressionsMode()) ? 0 : 1; + } + + private List getListMaxSize(List list) { + return list.size()> 10 ? list.subList(0, 10) : list; + } + + private List getImpressions(List impressionsListeners) { + List impressions = new ArrayList<>(); + for(IntegrationsConfig.ImpressionListenerWithMeta il: impressionsListeners) { + ImpressionListener listener = il.listener(); + if(listener instanceof NewRelicListener) { + impressions.add(NewRelicListener.class.getName()); + } + } + return impressions; + } +} diff --git a/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySyncTask.java b/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySyncTask.java new file mode 100644 index 000000000..8ad64dc5f --- /dev/null +++ b/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySyncTask.java @@ -0,0 +1,56 @@ +package io.split.telemetry.synchronizer; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; + +public class TelemetrySyncTask { + + private static final Logger _log = LoggerFactory.getLogger(TelemetrySyncTask.class); + private final ScheduledExecutorService _telemetrySyncScheduledExecutorService; + private final TelemetrySynchronizer _telemetrySynchronizer; + private final int _telemetryRefreshRate; + + public TelemetrySyncTask(int telemetryRefreshRate, TelemetrySynchronizer telemetrySynchronizer) { + ThreadFactory telemetrySyncThreadFactory = new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("Telemetry-sync-%d") + .build(); + _telemetrySynchronizer = checkNotNull(telemetrySynchronizer); + _telemetryRefreshRate = telemetryRefreshRate; + _telemetrySyncScheduledExecutorService = Executors.newSingleThreadScheduledExecutor(telemetrySyncThreadFactory); + try { + this.startScheduledTask(); + } catch (Exception e) { + _log.warn("Error trying to init telemetry stats synchronizer task."); + } + } + + @VisibleForTesting + protected void startScheduledTask() { + _telemetrySyncScheduledExecutorService.scheduleWithFixedDelay(() -> { + try { + _telemetrySynchronizer.synchronizeStats(); + } catch (Exception e) { + _log.warn("Error sending telemetry stats."); + } + },_telemetryRefreshRate, _telemetryRefreshRate, TimeUnit.SECONDS); + } + + public void stopScheduledTask(long splitCount, long segmentCount, long segmentKeyCount) { + try { + _telemetrySynchronizer.finalSynchronization(splitCount, segmentCount, segmentKeyCount); + } catch (Exception e) { + _log.warn("Error trying to send telemetry stats."); + } + _telemetrySyncScheduledExecutorService.shutdown(); + } +} diff --git a/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySynchronizer.java b/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySynchronizer.java new file mode 100644 index 000000000..7600a6334 --- /dev/null +++ b/client/src/main/java/io/split/telemetry/synchronizer/TelemetrySynchronizer.java @@ -0,0 +1,12 @@ +package io.split.telemetry.synchronizer; + +import io.split.client.SplitClientConfig; + +import java.util.List; +import java.util.Map; + +public interface TelemetrySynchronizer { + void synchronizeConfig(SplitClientConfig config, long timeUntilReady, Map factoryInstances, List tags); + void synchronizeStats() throws Exception; + void finalSynchronization(long splitCount, long segmentCount, long segmentKeyCount) throws Exception; +} diff --git a/client/src/main/java/io/split/telemetry/utils/AtomicLongArray.java b/client/src/main/java/io/split/telemetry/utils/AtomicLongArray.java new file mode 100644 index 000000000..eaad8b6dc --- /dev/null +++ b/client/src/main/java/io/split/telemetry/utils/AtomicLongArray.java @@ -0,0 +1,44 @@ +package io.split.telemetry.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.IntStream; + +public class AtomicLongArray { + private AtomicLong[] array; + private static final int MAX_LENGTH = 23; + + private static final Logger _log = LoggerFactory.getLogger(AtomicLongArray.class); + + public AtomicLongArray(int size) { + if(size <= 0) { + _log.error("Invalid array size. Using default size: " + MAX_LENGTH); + size = MAX_LENGTH; + } + array = new AtomicLong[size]; + IntStream.range(0, array.length).forEach(x -> array[x] = new AtomicLong()); + } + + public void increment(int index) { + if (index < 0 || index >= array.length) { + _log.error("Index is out of bounds. Did not incremented."); + return; + } + array[index].getAndIncrement(); + } + + public List fetchAndClearAll() { + List listValues = new ArrayList<>(); + for (AtomicLong a: array) { + listValues.add(a.longValue()); + } + + IntStream.range(0, array.length).forEach(x -> array[x] = new AtomicLong()); + + return listValues; + } +} diff --git a/client/src/main/java/io/split/telemetry/utils/BucketCalculator.java b/client/src/main/java/io/split/telemetry/utils/BucketCalculator.java new file mode 100644 index 000000000..7c5ce3ced --- /dev/null +++ b/client/src/main/java/io/split/telemetry/utils/BucketCalculator.java @@ -0,0 +1,64 @@ +package io.split.telemetry.utils; + +import java.util.Arrays; + +/** + * Calculates buckets from latency + *

+ * (1) 1.00 + * (2) 1.50 + * (3) 2.25 + * (4) 3.38 + * (5) 5.06 + * (6) 7.59 + * (7) 11.39 + * (8) 17.09 + * (9) 25.63 + * (10) 38.44 + * (11) 57.67 + * (12) 86.50 + * (13) 129.75 + * (14) 194.62 + * (15) 291.93 + * (16) 437.89 + * (17) 656.84 + * (18) 985.26 + * (19) 1,477.89 + * (20) 2,216.84 + * (21) 3,325.26 + * (22) 4,987.89 + * (23) 7,481.83 + *

+ */ +public class BucketCalculator { + + static final long[] BUCKETS = { + 1000, 1500, 2250, 3375, 5063, + 7594, 11391, 17086, 25629, 38443, + 57665, 86498, 129746, 194620, 291929, + 437894, 656841, 985261, 1477892, 2216838, + 3325257, 4987885, 7481828 + }; + + static final long MAX_LATENCY = 7481828; + + public static int getBucketForLatency(long latency) { + long micros = latency / 1000; //Convert to milliseconds + if (micros > MAX_LATENCY) { + return BUCKETS.length - 1; + } + + int index = Arrays.binarySearch(BUCKETS, micros); + + if (index < 0) { + + // Adjust the index based on Java Array javadocs. <0 means the value wasn't found and it's module value + // is where it should be inserted (in this case, it means the counter it applies - unless it's equals to the + // length of the array). + + index = -(index + 1); + } + return index; + } + +} diff --git a/client/src/test/java/io/split/TestHelper.java b/client/src/test/java/io/split/TestHelper.java index b0e424a56..39b973c78 100644 --- a/client/src/test/java/io/split/TestHelper.java +++ b/client/src/test/java/io/split/TestHelper.java @@ -3,6 +3,7 @@ import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.core5.http.ClassicHttpResponse; +import org.apache.hc.core5.http.Header; import org.apache.hc.core5.http.HttpEntity; import org.mockito.Mockito; @@ -18,14 +19,14 @@ public static CloseableHttpClient mockHttpClient(String jsonName, int httpStatus ClassicHttpResponse httpResponseMock = Mockito.mock(ClassicHttpResponse.class); Mockito.when(httpResponseMock.getEntity()).thenReturn(entityMock); Mockito.when(httpResponseMock.getCode()).thenReturn(httpStatus); - + Mockito.when(httpResponseMock.getHeaders()).thenReturn(new Header[0]); CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class); Mockito.when(httpClientMock.execute(Mockito.anyObject())).thenReturn(classicResponseToCloseableMock(httpResponseMock)); return httpClientMock; } - private static CloseableHttpResponse classicResponseToCloseableMock(ClassicHttpResponse mocked) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { + public static CloseableHttpResponse classicResponseToCloseableMock(ClassicHttpResponse mocked) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException { Method adaptMethod = CloseableHttpResponse.class.getDeclaredMethod("adapt", ClassicHttpResponse.class); adaptMethod.setAccessible(true); return (CloseableHttpResponse) adaptMethod.invoke(null, mocked); diff --git a/client/src/test/java/io/split/cache/SegmentCacheInMemoryImplTest.java b/client/src/test/java/io/split/cache/SegmentCacheInMemoryImplTest.java index 95f0b54ee..1e5fcd4c3 100644 --- a/client/src/test/java/io/split/cache/SegmentCacheInMemoryImplTest.java +++ b/client/src/test/java/io/split/cache/SegmentCacheInMemoryImplTest.java @@ -1,6 +1,7 @@ package io.split.cache; import junit.framework.TestCase; +import org.junit.Assert; import org.junit.Test; import java.util.ArrayList; @@ -58,4 +59,20 @@ public void testClear() { segmentCacheInMemory.clear(); assertEquals(DEFAULT_CHANGE_NUMBER, segmentCacheInMemory.getChangeNumber(SEGMENT_NAME)); } + + @Test + public void testGetAll() { + SegmentCacheInMemoryImpl segmentCacheInMemory = new SegmentCacheInMemoryImpl(); + segmentCacheInMemory.updateSegment(SEGMENT_NAME,new ArrayList<>(), new ArrayList<>()); + segmentCacheInMemory.updateSegment(FAKE_SEGMENT_NAME,new ArrayList<>(), new ArrayList<>()); + Assert.assertEquals(2, segmentCacheInMemory.getAll().stream().count()); + } + + @Test + public void testGetAllKeys() { + SegmentCacheInMemoryImpl segmentCacheInMemory = new SegmentCacheInMemoryImpl(); + segmentCacheInMemory.updateSegment(SEGMENT_NAME,Stream.of("KEY1", "KEY2").collect(Collectors.toList()), new ArrayList<>()); + segmentCacheInMemory.updateSegment(FAKE_SEGMENT_NAME,Stream.of("KEY3", "KEY2").collect(Collectors.toList()), new ArrayList<>()); + Assert.assertEquals(4, segmentCacheInMemory.getKeyCount()); + } } \ No newline at end of file diff --git a/client/src/test/java/io/split/client/ApiKeyCounterTest.java b/client/src/test/java/io/split/client/ApiKeyCounterTest.java index c017127ce..1513313b8 100644 --- a/client/src/test/java/io/split/client/ApiKeyCounterTest.java +++ b/client/src/test/java/io/split/client/ApiKeyCounterTest.java @@ -1,50 +1,107 @@ package io.split.client; import junit.framework.TestCase; +import org.junit.After; +import org.junit.Assert; import org.junit.Test; +import java.util.Map; + public class ApiKeyCounterTest extends TestCase { private static final String FIRST_KEY = "KEYNUMBER1"; private static final String SECOND_KEY = "KEYNUMBER2"; + @After + public synchronized void clearApiKeys() { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + } + + @Test + public synchronized void testAddingNewToken() { + try { + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + assertTrue(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); + } + finally { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + } + } + @Test - public void testAddingNewToken() { - ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); - assertTrue(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); + public synchronized void testAddingExistingToken() { + try { + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); - ApiKeyCounter.getApiKeyCounterInstance().remove(FIRST_KEY); + assertTrue(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); + assertEquals(2, ApiKeyCounter.getApiKeyCounterInstance().getCount(FIRST_KEY)); + } + finally { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + } } @Test - public void testAddingExistingToken() { - ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); - ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); - - assertTrue(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); - assertEquals(2, ApiKeyCounter.getApiKeyCounterInstance().getCount(FIRST_KEY)); - ApiKeyCounter.getApiKeyCounterInstance().remove(FIRST_KEY); - ApiKeyCounter.getApiKeyCounterInstance().remove(FIRST_KEY); + public synchronized void testRemovingToken() { + try { + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().remove(FIRST_KEY); + + assertFalse(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); + assertEquals(0, ApiKeyCounter.getApiKeyCounterInstance().getCount(FIRST_KEY)); + } + finally { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + } + } + + @Test + public synchronized void testAddingNonExistingToken() { + try { + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(SECOND_KEY); + + assertTrue(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); + assertEquals(1, ApiKeyCounter.getApiKeyCounterInstance().getCount(FIRST_KEY)); + assertEquals(1, ApiKeyCounter.getApiKeyCounterInstance().getCount(SECOND_KEY)); + } + finally { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + } } @Test - public void testRemovingToken() { - ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); - ApiKeyCounter.getApiKeyCounterInstance().remove(FIRST_KEY); + public synchronized void testFactoryInstances() { + try { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(SECOND_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(SECOND_KEY); - assertFalse(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); - assertEquals(0, ApiKeyCounter.getApiKeyCounterInstance().getCount(FIRST_KEY)); + Map factoryInstances = ApiKeyCounter.getApiKeyCounterInstance().getFactoryInstances(); + Assert.assertEquals(2, factoryInstances.size()); + Assert.assertEquals(3, factoryInstances.get(FIRST_KEY).intValue()); + Assert.assertEquals(2, factoryInstances.get(SECOND_KEY).intValue()); + } + finally { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + } } @Test - public void testAddingNonExistingToken() { - ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); - ApiKeyCounter.getApiKeyCounterInstance().add(SECOND_KEY); - - assertTrue(ApiKeyCounter.getApiKeyCounterInstance().isApiKeyPresent(FIRST_KEY)); - assertEquals(1, ApiKeyCounter.getApiKeyCounterInstance().getCount(FIRST_KEY)); - assertEquals(1, ApiKeyCounter.getApiKeyCounterInstance().getCount(SECOND_KEY)); - ApiKeyCounter.getApiKeyCounterInstance().remove(FIRST_KEY); - ApiKeyCounter.getApiKeyCounterInstance().remove(SECOND_KEY); + public synchronized void testClearApiKey() { + try { + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + Assert.assertEquals(0, ApiKeyCounter.getApiKeyCounterInstance().getCount(FIRST_KEY)); + } + finally { + ApiKeyCounter.getApiKeyCounterInstance().clearApiKeys(); + } } } diff --git a/client/src/test/java/io/split/client/EventsClientImplTest.java b/client/src/test/java/io/split/client/EventsClientImplTest.java index 1b2706ac1..2bc9d5553 100644 --- a/client/src/test/java/io/split/client/EventsClientImplTest.java +++ b/client/src/test/java/io/split/client/EventsClientImplTest.java @@ -1,26 +1,31 @@ package io.split.client; import io.split.client.dtos.Event; +import io.split.telemetry.domain.enums.EventsDataRecordsEnum; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; import org.apache.hc.client5.http.classic.methods.HttpUriRequest; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.hamcrest.Matchers; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; - import java.util.concurrent.LinkedBlockingQueue; public class EventsClientImplTest { + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + @Test public void testDefaultURL() throws URISyntaxException { URI rootTarget = URI.create("https://api.split.io"); CloseableHttpClient httpClient = HttpClients.custom().build(); - EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5); + EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://api.split.io/api/events/bulk"))); } @@ -28,7 +33,7 @@ public void testDefaultURL() throws URISyntaxException { public void testCustomURLNoPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com"); CloseableHttpClient httpClient = HttpClients.custom().build(); - EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5); + EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/api/events/bulk"))); } @@ -36,7 +41,7 @@ public void testCustomURLNoPathNoBackslash() throws URISyntaxException { public void testCustomURLAppendingPath() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split/"); CloseableHttpClient httpClient = HttpClients.custom().build(); - EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5); + EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/events/bulk"))); } @@ -44,7 +49,7 @@ public void testCustomURLAppendingPath() throws URISyntaxException { public void testCustomURLAppendingPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split"); CloseableHttpClient httpClient = HttpClients.custom().build(); - EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5); + EventClientImpl fetcher = EventClientImpl.create(httpClient, rootTarget, 5, 5, 5, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/events/bulk"))); } @@ -56,7 +61,7 @@ public void testEventsFlushedWhenSizeLimitReached() throws URISyntaxException, I URI.create("https://kubernetesturl.com/split"), 10000, // Long queue so it doesn't flush by # of events 100000, // Long period so it doesn't flush by timeout expiration. - 0); + 0, TELEMETRY_STORAGE); for (int i = 0; i < 159; ++i) { Event event = new Event(); @@ -71,4 +76,25 @@ public void testEventsFlushedWhenSizeLimitReached() throws URISyntaxException, I Thread.sleep(2000); Mockito.verify(client, Mockito.times(1)).execute((HttpUriRequest) Mockito.any()); } + + @Test + public void testEventDropped() throws URISyntaxException, NoSuchFieldException, IllegalAccessException, InterruptedException { + TelemetryStorage telemetryStorage = Mockito.mock(InMemoryTelemetryStorage.class); + CloseableHttpClient client = Mockito.mock(CloseableHttpClient.class); + EventClientImpl eventClient = new EventClientImpl(new LinkedBlockingQueue<>(2), + client, + URI.create("https://kubernetesturl.com/split"), + 10000, // Long queue so it doesn't flush by # of events + 100000, // Long period so it doesn't flush by timeout expiration. + 0, telemetryStorage); + eventClient.close(); + Thread.sleep(1000); + for (int i = 0; i < 3; ++i) { + Event event = new Event(); + eventClient.track(event, 1); + } + + Mockito.verify(telemetryStorage, Mockito.times(2)).recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 1); + Mockito.verify(telemetryStorage, Mockito.times(1)).recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 1); + } } diff --git a/client/src/test/java/io/split/client/HttpSegmentChangeFetcherTest.java b/client/src/test/java/io/split/client/HttpSegmentChangeFetcherTest.java index afb238552..be1fb4b3c 100644 --- a/client/src/test/java/io/split/client/HttpSegmentChangeFetcherTest.java +++ b/client/src/test/java/io/split/client/HttpSegmentChangeFetcherTest.java @@ -2,26 +2,37 @@ import io.split.TestHelper; import io.split.client.dtos.SegmentChange; +import io.split.engine.common.FetchOptions; import io.split.engine.metrics.Metrics; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.*; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; import java.io.IOException; +import java.io.StringBufferInputStream; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; +import java.util.List; + +import static org.mockito.Mockito.when; public class HttpSegmentChangeFetcherTest { + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + @Test public void testDefaultURL() throws URISyntaxException { URI rootTarget = URI.create("https://api.split.io"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://api.split.io/api/segmentChanges"))); } @@ -30,7 +41,7 @@ public void testCustomURLNoPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/segmentChanges"))); } @@ -39,7 +50,7 @@ public void testCustomURLAppendingPath() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split/"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/segmentChanges"))); } @@ -48,7 +59,7 @@ public void testCustomURLAppendingPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/segmentChanges"))); } @@ -59,9 +70,9 @@ public void testFetcherWithSpecialCharacters() throws URISyntaxException, IOExce CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("segment-change-special-chatacters.json", HttpStatus.SC_OK); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClientMock, rootTarget, metrics); + HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClientMock, rootTarget, TELEMETRY_STORAGE); - SegmentChange change = fetcher.fetch("some_segment", 1234567, true); + SegmentChange change = fetcher.fetch("some_segment", 1234567, new FetchOptions.Builder().build()); Assert.assertNotNull(change); Assert.assertEquals(1, change.added.size()); @@ -69,4 +80,31 @@ public void testFetcherWithSpecialCharacters() throws URISyntaxException, IOExce Assert.assertEquals(1, change.removed.size()); Assert.assertEquals("other_user", change.removed.get(0)); } + + @Test + public void testFetcherWithCDNBypassOption() throws IOException, URISyntaxException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + URI rootTarget = URI.create("https://api.split.io"); + + HttpEntity entityMock = Mockito.mock(HttpEntity.class); + when(entityMock.getContent()).thenReturn(new StringBufferInputStream("{\"till\": 1}")); + ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class); + when(response.getCode()).thenReturn(200); + when(response.getEntity()).thenReturn(entityMock); + when(response.getHeaders()).thenReturn(new Header[0]); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class); + CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class); + when(httpClientMock.execute(requestCaptor.capture())).thenReturn(TestHelper.classicResponseToCloseableMock(response)); + + Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); + HttpSegmentChangeFetcher fetcher = HttpSegmentChangeFetcher.create(httpClientMock, rootTarget, Mockito.mock(TelemetryStorage.class)); + + fetcher.fetch("someSegment", -1, new FetchOptions.Builder().targetChangeNumber(123).build()); + fetcher.fetch("someSegment2",-1, new FetchOptions.Builder().build()); + List captured = requestCaptor.getAllValues(); + Assert.assertEquals(captured.size(), 2); + Assert.assertTrue(captured.get(0).getUri().toString().contains("till=123")); + Assert.assertFalse(captured.get(1).getUri().toString().contains("till=")); + } + } diff --git a/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java b/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java index 564339db7..55090e7b3 100644 --- a/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java +++ b/client/src/test/java/io/split/client/HttpSplitChangeFetcherTest.java @@ -3,27 +3,45 @@ import io.split.TestHelper; import io.split.client.dtos.Split; import io.split.client.dtos.SplitChange; +import io.split.engine.common.FetchOptions; import io.split.engine.metrics.Metrics; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.telemetry.storage.TelemetryStorage; +import org.apache.hc.client5.http.classic.methods.HttpUriRequestBase; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse; import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.apache.hc.core5.http.HttpStatus; +import org.apache.hc.core5.http.*; +import org.apache.hc.core5.http.io.entity.StringEntity; +import org.apache.hc.core5.http.message.BasicClassicHttpResponse; import org.hamcrest.Matchers; import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; +import java.io.Closeable; import java.io.IOException; +import java.io.StringBufferInputStream; import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.net.URISyntaxException; +import java.util.HashSet; +import java.util.List; import java.util.Map; +import java.util.Set; + +import static org.mockito.Mockito.when; public class HttpSplitChangeFetcherTest { + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); @Test public void testDefaultURL() throws URISyntaxException { URI rootTarget = URI.create("https://api.split.io"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://api.split.io/api/splitChanges"))); } @@ -32,7 +50,7 @@ public void testCustomURLNoPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/splitChanges"))); } @@ -41,7 +59,7 @@ public void testCustomURLAppendingPath() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split/"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/splitChanges"))); } @@ -50,7 +68,7 @@ public void testCustomURLAppendingPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split"); CloseableHttpClient httpClient = HttpClients.custom().build(); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, metrics); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClient, rootTarget, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/splitChanges"))); } @@ -61,9 +79,9 @@ public void testFetcherWithSpecialCharacters() throws URISyntaxException, Invoca CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("split-change-special-characters.json", HttpStatus.SC_OK); Metrics.NoopMetrics metrics = new Metrics.NoopMetrics(); - HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClientMock, rootTarget, metrics); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClientMock, rootTarget, TELEMETRY_STORAGE); - SplitChange change = fetcher.fetch(1234567, true); + SplitChange change = fetcher.fetch(1234567, new FetchOptions.Builder().cacheControlHeaders(true).build()); Assert.assertNotNull(change); Assert.assertEquals(1, change.splits.size()); @@ -75,4 +93,47 @@ public void testFetcherWithSpecialCharacters() throws URISyntaxException, Invoca Assert.assertEquals("{\"test\": \"blue\",\"grĂĽne StraĂźe\": 13}", configs.get("on")); Assert.assertEquals("{\"test\": \"blue\",\"size\": 15}", configs.get("off")); } + + @Test + public void testFetcherWithCDNBypassOption() throws IOException, URISyntaxException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { + URI rootTarget = URI.create("https://api.split.io"); + + HttpEntity entityMock = Mockito.mock(HttpEntity.class); + when(entityMock.getContent()).thenReturn(new StringBufferInputStream("{\"till\": 1}")); + ClassicHttpResponse response = Mockito.mock(ClassicHttpResponse.class); + when(response.getCode()).thenReturn(200); + when(response.getEntity()).thenReturn(entityMock); + when(response.getHeaders()).thenReturn(new Header[0]); + + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(ClassicHttpRequest.class); + CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class); + when(httpClientMock.execute(requestCaptor.capture())).thenReturn(TestHelper.classicResponseToCloseableMock(response)); + + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClientMock, rootTarget, Mockito.mock(TelemetryRuntimeProducer.class)); + + fetcher.fetch(-1, new FetchOptions.Builder().targetChangeNumber(123).build()); + fetcher.fetch(-1, new FetchOptions.Builder().build()); + List captured = requestCaptor.getAllValues(); + Assert.assertEquals(captured.size(), 2); + Assert.assertTrue(captured.get(0).getUri().toString().contains("till=123")); + Assert.assertFalse(captured.get(1).getUri().toString().contains("till=")); + } + + @Test + public void testRandomNumberGeneration() throws URISyntaxException { + URI rootTarget = URI.create("https://api.split.io"); + CloseableHttpClient httpClientMock = Mockito.mock(CloseableHttpClient.class); + HttpSplitChangeFetcher fetcher = HttpSplitChangeFetcher.create(httpClientMock, rootTarget, Mockito.mock(TelemetryRuntimeProducer.class)); + + Set seen = new HashSet<>(); + long min = (long)Math.pow(2, 63) * (-1); + final long total = 10000000; + for (long x = 0; x < total; x++) { + long r = fetcher.makeRandomTill(); + Assert.assertTrue(r < 0 && r > min); + seen.add(r); + } + + Assert.assertTrue(seen.size() >= (total * 0.9999)); + } } diff --git a/client/src/test/java/io/split/client/SplitClientImplTest.java b/client/src/test/java/io/split/client/SplitClientImplTest.java index 4e1370ad4..acb1f304b 100644 --- a/client/src/test/java/io/split/client/SplitClientImplTest.java +++ b/client/src/test/java/io/split/client/SplitClientImplTest.java @@ -23,12 +23,13 @@ import io.split.engine.matchers.GreaterThanOrEqualToMatcher; import io.split.engine.matchers.collections.ContainsAnyOfSetMatcher; import io.split.engine.matchers.strings.WhitelistMatcher; -import io.split.engine.metrics.Metrics; import io.split.grammar.Treatments; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; import org.apache.commons.lang3.RandomStringUtils; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; @@ -61,8 +62,14 @@ */ public class SplitClientImplTest { + private static TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); private SplitClientConfig config = SplitClientConfig.builder().setBlockUntilReadyTimeout(100).build(); + @Before + public void updateTelemetryStorage() { + TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + } + @Test public void null_key_results_in_control() { String test = "test1"; @@ -78,11 +85,10 @@ public void null_key_results_in_control() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment(null, "test1"), is(equalTo(Treatments.CONTROL))); @@ -105,11 +111,10 @@ public void null_test_results_in_control() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@relateiq.com", null), is(equalTo(Treatments.CONTROL))); @@ -127,11 +132,10 @@ public void exceptions_result_in_control() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@relateiq.com", "test1"), is(equalTo(Treatments.CONTROL))); @@ -149,16 +153,16 @@ public void works() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCache splitCache = mock(InMemoryCacheImp.class); when(splitCache.get(test)).thenReturn(parsedSplit); + when(gates.isSDKReady()).thenReturn(true); SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); int numKeys = 5; @@ -168,6 +172,7 @@ public void works() { } verify(splitCache, times(numKeys)).get(test); + verify(TELEMETRY_STORAGE, times(5)).recordLatency(Mockito.anyObject(), Mockito.anyLong()); } /** @@ -189,11 +194,10 @@ public void works_null_config() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); @@ -226,11 +230,10 @@ public void worksAndHasConfig() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); int numKeys = 5; @@ -261,11 +264,10 @@ public void last_condition_is_always_default() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("pato@codigo.com", test), is(equalTo(Treatments.OFF))); @@ -297,11 +299,10 @@ public void last_condition_is_always_default_but_with_treatment() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); SplitResult result = client.getTreatmentWithConfig("pato@codigo.com", test); @@ -325,16 +326,16 @@ public void multiple_conditions_work() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCache splitCache = mock(InMemoryCacheImp.class); when(splitCache.get(test)).thenReturn(parsedSplit); + when(gates.isSDKReady()).thenReturn(false); SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@codigo.com", test), is(equalTo("on"))); @@ -342,6 +343,7 @@ public void multiple_conditions_work() { assertThat(client.getTreatment("trevor@codigo.com", test), is(equalTo("on"))); verify(splitCache, times(3)).get(test); + verify(TELEMETRY_STORAGE, times(3)).recordNonReadyUsage(); } @@ -361,11 +363,10 @@ public void killed_test_always_goes_to_default() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@codigo.com", test), is(equalTo(Treatments.OFF))); @@ -397,11 +398,10 @@ public void killed_test_always_goes_to_default_has_config() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); SplitResult result = client.getTreatmentWithConfig("adil@codigo.com", test); @@ -433,11 +433,10 @@ public void dependency_matcher_on() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("key", parent), is(equalTo(Treatments.ON))); @@ -466,11 +465,10 @@ public void dependency_matcher_off() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("key", parent), is(equalTo(Treatments.ON))); @@ -493,11 +491,10 @@ public void dependency_matcher_control() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("key", dependent), is(equalTo(Treatments.ON))); @@ -521,11 +518,10 @@ public void attributes_work() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@codigo.com", test), is(equalTo("on"))); @@ -555,11 +551,10 @@ public void attributes_work_2() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@codigo.com", test), is(equalTo("off"))); @@ -589,11 +584,10 @@ public void attributes_greater_than_negative_number() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@codigo.com", test), is(equalTo("off"))); @@ -626,11 +620,10 @@ public void attributes_for_sets() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("adil@codigo.com", test), is(equalTo("off"))); @@ -669,11 +662,10 @@ public void labels_are_populated() { mock(SplitFactory.class), splitCache, impressionsManager, - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Map attributes = ImmutableMap.of("age", -20, "acv", "1000000"); @@ -761,11 +753,10 @@ private void traffic_allocation(String key, int trafficAllocation, int trafficAl mock(SplitFactory.class), splitCache, impressionsManager, - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment(key, test), is(equalTo(expected_treatment_on_or_off))); @@ -809,11 +800,10 @@ public void notInTrafficAllocationDefaultConfig() { mock(SplitFactory.class), splitCache, impressionsManager, - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); assertThat(client.getTreatment("pato@split.io", test), is(equalTo(Treatments.OFF))); @@ -849,11 +839,10 @@ public void matching_bucketing_keys_work() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Key bad_key = new Key("adil", "aijaz"); @@ -887,11 +876,10 @@ public void impression_metadata_is_propagated() { mock(SplitFactory.class), splitCache, impressionsManager, - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Map attributes = ImmutableMap.of("age", -20, "acv", "1000000"); @@ -917,17 +905,16 @@ private Partition partition(String treatment, int size) { public void block_until_ready_does_not_time_when_sdk_is_ready() throws TimeoutException, InterruptedException { SplitCache splitCache = mock(InMemoryCacheImp.class); SDKReadinessGates ready = mock(SDKReadinessGates.class); - when(ready.isSDKReady(100)).thenReturn(true); + when(ready.waitUntilInternalReady(100)).thenReturn(true); SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCache, mock(ImpressionsManager.class), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, ready, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); client.blockUntilReady(); @@ -937,17 +924,16 @@ public void block_until_ready_does_not_time_when_sdk_is_ready() throws TimeoutEx public void block_until_ready_times_when_sdk_is_not_ready() throws TimeoutException, InterruptedException { SplitCache splitCache = mock(InMemoryCacheImp.class); SDKReadinessGates ready = mock(SDKReadinessGates.class); - when(ready.isSDKReady(100)).thenReturn(false); + when(ready.waitUntilInternalReady(100)).thenReturn(false); SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCache, mock(ImpressionsManager.class), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, ready, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); client.blockUntilReady(); @@ -957,16 +943,15 @@ public void block_until_ready_times_when_sdk_is_not_ready() throws TimeoutExcept public void track_with_valid_parameters() { SDKReadinessGates gates = mock(SDKReadinessGates.class); SplitCache splitCache = mock(InMemoryCacheImp.class); - + when(gates.isSDKReady()).thenReturn(false); SplitClientImpl client = new SplitClientImpl( mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Assert.assertThat(client.track("validKey", "valid_traffic_type", "valid_event"), @@ -976,6 +961,7 @@ public void track_with_valid_parameters() { String validKeySize = new String(new char[250]).replace('\0', 'a'); Assert.assertThat(client.track(validKeySize, "valid_traffic_type", validEventSize, 10), org.hamcrest.Matchers.is(org.hamcrest.Matchers.equalTo(true))); + verify(TELEMETRY_STORAGE, times(2)).recordLatency(Mockito.anyObject(), Mockito.anyLong()); } @@ -988,11 +974,10 @@ public void track_with_invalid_event_type_ids() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Assert.assertThat(client.track("validKey", "valid_traffic_type", ""), @@ -1019,11 +1004,10 @@ public void track_with_invalid_traffic_type_names() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Assert.assertThat(client.track("validKey", "", "valid"), @@ -1042,11 +1026,10 @@ public void track_with_invalid_keys() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Assert.assertThat(client.track("", "valid_traffic_type", "valid"), @@ -1071,11 +1054,10 @@ public void track_with_properties() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), eventClientMock, config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); HashMap properties = new HashMap<>(); @@ -1182,11 +1164,10 @@ public void getTreatment_with_invalid_keys() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Assert.assertThat(client.getTreatment("valid", "split"), @@ -1267,11 +1248,10 @@ public void client_cannot_perform_actions_when_destroyed() throws InterruptedExc mockFactory, splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); Assert.assertThat(client.getTreatment("valid", "split"), @@ -1310,11 +1290,10 @@ public void worksAndHasConfigTryKetTreatmentWithKey() { mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); int numKeys = 5; @@ -1347,11 +1326,10 @@ public void blockUntilReadyException() throws TimeoutException, InterruptedExcep mock(SplitFactory.class), splitCache, new ImpressionsManager.NoOpImpressionsManager(), - new Metrics.NoopMetrics(), NoopEventClient.create(), config, gates, - new EvaluatorImp(splitCache) + new EvaluatorImp(splitCache), TELEMETRY_STORAGE, TELEMETRY_STORAGE ); client.blockUntilReady(); diff --git a/client/src/test/java/io/split/client/SplitClientIntegrationTest.java b/client/src/test/java/io/split/client/SplitClientIntegrationTest.java index d7ab9ef65..aba335b57 100644 --- a/client/src/test/java/io/split/client/SplitClientIntegrationTest.java +++ b/client/src/test/java/io/split/client/SplitClientIntegrationTest.java @@ -3,12 +3,15 @@ import io.split.SSEMockServer; import io.split.SplitMockServer; import io.split.client.api.SplitView; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; import org.awaitility.Awaitility; import org.glassfish.grizzly.utils.Pair; import org.glassfish.jersey.media.sse.OutboundEvent; import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; +import org.mockito.Mockito; import javax.ws.rs.sse.OutboundSseEvent; import java.io.IOException; @@ -20,9 +23,11 @@ public class SplitClientIntegrationTest { // TODO: review this test. + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + @Test @Ignore - public void getTreatmentWithStreamingEnabled() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void getTreatmentWithStreamingEnabled() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); @@ -111,7 +116,7 @@ public void getTreatmentWithStreamingEnabled() throws IOException, TimeoutExcept } @Test - public void getTreatmentWithStreamingEnabledAndAuthDisabled() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void getTreatmentWithStreamingEnabledAndAuthDisabled() throws Exception { SplitMockServer splitServer = new SplitMockServer(); splitServer.start(); @@ -134,7 +139,7 @@ public void getTreatmentWithStreamingEnabledAndAuthDisabled() throws IOException } @Test - public void getTreatmentWithStreamingDisabled() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void getTreatmentWithStreamingDisabled() throws Exception { SplitMockServer splitServer = new SplitMockServer(); splitServer.start(); @@ -162,7 +167,7 @@ public void getTreatmentWithStreamingDisabled() throws IOException, TimeoutExcep } @Test - public void managerSplitsWithStreamingEnabled() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void managerSplitsWithStreamingEnabled() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); @@ -197,7 +202,7 @@ public void managerSplitsWithStreamingEnabled() throws IOException, TimeoutExcep } @Test - public void splitClientOccupancyNotifications() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void splitClientOccupancyNotifications() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); @@ -259,7 +264,7 @@ public void splitClientOccupancyNotifications() throws IOException, TimeoutExcep } @Test - public void splitClientControlNotifications() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void splitClientControlNotifications() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); @@ -341,7 +346,7 @@ public void splitClientControlNotifications() throws IOException, TimeoutExcepti } @Test - public void splitClientMultiFactory() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void splitClientMultiFactory() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue1 = new SSEMockServer.SseEventQueue(); @@ -465,7 +470,7 @@ public void splitClientMultiFactory() throws IOException, TimeoutException, Inte // TODO: review this test. @Test @Ignore - public void keepAlive() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void keepAlive() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); @@ -495,7 +500,7 @@ public void keepAlive() throws IOException, TimeoutException, InterruptedExcepti } @Test - public void testConnectionClosedByRemoteHostIsProperlyHandled() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void testConnectionClosedByRemoteHostIsProperlyHandled() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); @@ -528,7 +533,7 @@ public void testConnectionClosedByRemoteHostIsProperlyHandled() throws IOExcepti } @Test - public void testConnectionClosedIsProperlyHandled() throws IOException, TimeoutException, InterruptedException, URISyntaxException { + public void testConnectionClosedIsProperlyHandled() throws Exception { SplitMockServer splitServer = new SplitMockServer(); SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); diff --git a/client/src/test/java/io/split/client/SplitFactoryImplTest.java b/client/src/test/java/io/split/client/SplitFactoryImplTest.java index 451bf7dcc..99123aa93 100644 --- a/client/src/test/java/io/split/client/SplitFactoryImplTest.java +++ b/client/src/test/java/io/split/client/SplitFactoryImplTest.java @@ -1,10 +1,15 @@ package io.split.client; import io.split.client.impressions.ImpressionsManager; +import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.integrations.IntegrationsConfig; +import io.split.telemetry.storage.TelemetryStorage; import junit.framework.TestCase; import org.junit.Test; +import org.mockito.Mockito; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.net.URISyntaxException; public class SplitFactoryImplTest extends TestCase { @@ -15,7 +20,7 @@ public class SplitFactoryImplTest extends TestCase { @Test - public void testFactoryInstantiation() throws URISyntaxException { + public void testFactoryInstantiation() throws Exception { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) @@ -23,6 +28,7 @@ public void testFactoryInstantiation() throws URISyntaxException { .endpoint(ENDPOINT,EVENTS_ENDPOINT) .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(10000) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .build(); SplitFactoryImpl splitFactory = new SplitFactoryImpl(API_KEY, splitClientConfig); @@ -31,12 +37,13 @@ public void testFactoryInstantiation() throws URISyntaxException { } @Test - public void testFactoryInstantiationWithoutBlockUntilReady() throws URISyntaxException { + public void testFactoryInstantiationWithoutBlockUntilReady() throws Exception { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .authServiceURL(AUTH_SERVICE) .build(); SplitFactoryImpl splitFactory = new SplitFactoryImpl(API_KEY, splitClientConfig); @@ -46,13 +53,14 @@ public void testFactoryInstantiationWithoutBlockUntilReady() throws URISyntaxExc } @Test - public void testFactoryInstantiationIntegrationsConfig() throws URISyntaxException { + public void testFactoryInstantiationIntegrationsConfig() throws Exception { IntegrationsConfig integrationsConfig = new IntegrationsConfig.Builder().build(); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(1000) .integrations(integrationsConfig) @@ -64,12 +72,13 @@ public void testFactoryInstantiationIntegrationsConfig() throws URISyntaxExcepti } @Test - public void testFactoryInstantiationWithProxy() throws URISyntaxException { + public void testFactoryInstantiationWithProxy() throws Exception { SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(1000) .proxyPort(6060) @@ -84,19 +93,31 @@ public void testFactoryInstantiationWithProxy() throws URISyntaxException { } @Test - public void testFactoryDestroy() throws URISyntaxException { + public void testFactoryDestroy() throws Exception { + TelemetryStorage telemetryStorage = Mockito.mock(TelemetryStorage.class); SplitClientConfig splitClientConfig = SplitClientConfig.builder() .enableDebug() .impressionsMode(ImpressionsManager.Mode.DEBUG) .impressionsRefreshRate(1) .endpoint(ENDPOINT,EVENTS_ENDPOINT) + .telemetryURL(SplitClientConfig.TELEMETRY_ENDPOINT) .authServiceURL(AUTH_SERVICE) .setBlockUntilReadyTimeout(10000) .build(); + SplitFactoryImpl splitFactory = new SplitFactoryImpl(API_KEY, splitClientConfig); + //Before destroy we replace telemetryStorage via reflection. + Field factoryDestroy = SplitFactoryImpl.class.getDeclaredField("_telemetryStorage"); + factoryDestroy.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(factoryDestroy, factoryDestroy.getModifiers() & ~Modifier.FINAL); + + factoryDestroy.set(splitFactory, telemetryStorage); splitFactory.destroy(); assertTrue(splitFactory.isDestroyed()); + Mockito.verify(telemetryStorage, Mockito.times(1)).recordSessionLength(Mockito.anyLong()); } } \ No newline at end of file diff --git a/client/src/test/java/io/split/client/SplitManagerImplTest.java b/client/src/test/java/io/split/client/SplitManagerImplTest.java index 7ef068ac9..201f1fedc 100644 --- a/client/src/test/java/io/split/client/SplitManagerImplTest.java +++ b/client/src/test/java/io/split/client/SplitManagerImplTest.java @@ -10,6 +10,9 @@ import io.split.engine.matchers.AllKeysMatcher; import io.split.engine.matchers.CombiningMatcher; import io.split.grammar.Treatments; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -23,13 +26,17 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; public class SplitManagerImplTest { private SplitClientConfig config = SplitClientConfig.builder().setBlockUntilReadyTimeout(100).build(); + private static TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + @Before + public void updateTelemetryStorage() { + TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + } @Test public void splitCallWithNonExistentSplit() { String nonExistent = "nonExistent"; @@ -38,7 +45,7 @@ public void splitCallWithNonExistentSplit() { SplitManagerImpl splitManager = new SplitManagerImpl(splitCache, Mockito.mock(SplitClientConfig.class), - Mockito.mock(SDKReadinessGates.class)); + Mockito.mock(SDKReadinessGates.class), TELEMETRY_STORAGE); assertThat(splitManager.split("nonExistent"), is(nullValue())); } @@ -52,7 +59,7 @@ public void splitCallWithExistentSplit() { SplitManagerImpl splitManager = new SplitManagerImpl(splitCache, Mockito.mock(SplitClientConfig.class), - Mockito.mock(SDKReadinessGates.class)); + Mockito.mock(SDKReadinessGates.class), TELEMETRY_STORAGE); SplitView theOne = splitManager.split(existent); assertThat(theOne.name, is(equalTo(response.feature()))); assertThat(theOne.changeNumber, is(equalTo(response.changeNumber()))); @@ -77,7 +84,7 @@ public void splitCallWithExistentSplitAndConfigs() { SplitManagerImpl splitManager = new SplitManagerImpl(splitCache, Mockito.mock(SplitClientConfig.class), - Mockito.mock(SDKReadinessGates.class)); + Mockito.mock(SDKReadinessGates.class), TELEMETRY_STORAGE); SplitView theOne = splitManager.split(existent); assertThat(theOne.name, is(equalTo(response.feature()))); assertThat(theOne.changeNumber, is(equalTo(response.changeNumber()))); @@ -92,23 +99,28 @@ public void splitCallWithExistentSplitAndConfigs() { public void splitsCallWithNoSplit() { SplitCache splitCache = Mockito.mock(SplitCache.class); Mockito.when(splitCache.getAll()).thenReturn(Lists.newArrayList()); + SDKReadinessGates gates = Mockito.mock(SDKReadinessGates.class); + Mockito.when(gates.isSDKReady()).thenReturn(false); SplitManagerImpl splitManager = new SplitManagerImpl(splitCache, Mockito.mock(SplitClientConfig.class), - Mockito.mock(SDKReadinessGates.class)); + gates, TELEMETRY_STORAGE); assertThat(splitManager.splits(), is(empty())); + verify(TELEMETRY_STORAGE, times(1)).recordNonReadyUsage(); } @Test public void splitsCallWithSplit() { SplitCache splitCache = Mockito.mock(SplitCache.class); List parsedSplits = Lists.newArrayList(); + SDKReadinessGates gates = Mockito.mock(SDKReadinessGates.class); + Mockito.when(gates.isSDKReady()).thenReturn(false); ParsedSplit response = ParsedSplit.createParsedSplitForTests("FeatureName", 123, true, "off", Lists.newArrayList(getTestCondition("off")), "traffic", 456L, 1); parsedSplits.add(response); Mockito.when(splitCache.getAll()).thenReturn(parsedSplits); SplitManagerImpl splitManager = new SplitManagerImpl(splitCache, Mockito.mock(SplitClientConfig.class), - Mockito.mock(SDKReadinessGates.class)); + gates, TELEMETRY_STORAGE); List splits = splitManager.splits(); assertThat(splits.size(), is(equalTo(1))); assertThat(splits.get(0).name, is(equalTo(response.feature()))); @@ -117,16 +129,20 @@ public void splitsCallWithSplit() { assertThat(splits.get(0).trafficType, is(equalTo(response.trafficTypeName()))); assertThat(splits.get(0).treatments.size(), is(equalTo(1))); assertThat(splits.get(0).treatments.get(0), is(equalTo("off"))); + verify(TELEMETRY_STORAGE, times(1)).recordNonReadyUsage(); } @Test public void splitNamesCallWithNoSplit() { SplitCache splitCache = Mockito.mock(SplitCache.class); Mockito.when(splitCache.getAll()).thenReturn(Lists.newArrayList()); + SDKReadinessGates gates = Mockito.mock(SDKReadinessGates.class); + Mockito.when(gates.isSDKReady()).thenReturn(false); SplitManagerImpl splitManager = new SplitManagerImpl(splitCache, Mockito.mock(SplitClientConfig.class), - Mockito.mock(SDKReadinessGates.class)); + gates, TELEMETRY_STORAGE); assertThat(splitManager.splitNames(), is(empty())); + verify(TELEMETRY_STORAGE, times(1)).recordNonReadyUsage(); } @Test @@ -139,7 +155,7 @@ public void splitNamesCallWithSplit() { Mockito.when(splitCache.getAll()).thenReturn(parsedSplits); SplitManagerImpl splitManager = new SplitManagerImpl(splitCache, Mockito.mock(SplitClientConfig.class), - Mockito.mock(SDKReadinessGates.class)); + Mockito.mock(SDKReadinessGates.class), TELEMETRY_STORAGE); List splitNames = splitManager.splitNames(); assertThat(splitNames.size(), is(equalTo(1))); assertThat(splitNames.get(0), is(equalTo(response.feature()))); @@ -148,10 +164,10 @@ public void splitNamesCallWithSplit() { @Test public void block_until_ready_does_not_time_when_sdk_is_ready() throws TimeoutException, InterruptedException { SDKReadinessGates ready = mock(SDKReadinessGates.class); - when(ready.isSDKReady(100)).thenReturn(true); + when(ready.waitUntilInternalReady(100)).thenReturn(true); SplitManagerImpl splitManager = new SplitManagerImpl(mock(SplitCache.class), config, - ready); + ready, TELEMETRY_STORAGE); splitManager.blockUntilReady(); } @@ -159,13 +175,14 @@ public void block_until_ready_does_not_time_when_sdk_is_ready() throws TimeoutEx @Test(expected = TimeoutException.class) public void block_until_ready_times_when_sdk_is_not_ready() throws TimeoutException, InterruptedException { SDKReadinessGates ready = mock(SDKReadinessGates.class); - when(ready.isSDKReady(100)).thenReturn(false); + when(ready.waitUntilInternalReady(100)).thenReturn(false); SplitManagerImpl splitManager = new SplitManagerImpl(mock(SplitCache.class), config, - ready); + ready, TELEMETRY_STORAGE); splitManager.blockUntilReady(); + verify(TELEMETRY_STORAGE, times(1)).recordBURTimeout(); } private ParsedCondition getTestCondition(String treatment) { diff --git a/client/src/test/java/io/split/client/impressions/HttpImpressionsSenderTest.java b/client/src/test/java/io/split/client/impressions/HttpImpressionsSenderTest.java index 876e2bf3c..ee586ffe4 100644 --- a/client/src/test/java/io/split/client/impressions/HttpImpressionsSenderTest.java +++ b/client/src/test/java/io/split/client/impressions/HttpImpressionsSenderTest.java @@ -6,6 +6,8 @@ import io.split.client.dtos.ImpressionCount; import io.split.client.dtos.KeyImpression; import io.split.client.dtos.TestImpressions; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.classic.methods.HttpUriRequest; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; @@ -34,11 +36,13 @@ import static org.mockito.Mockito.verify; public class HttpImpressionsSenderTest { + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + @Test public void testDefaultURL() throws URISyntaxException { URI rootTarget = URI.create("https://api.split.io"); CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG); + HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://api.split.io/api/testImpressions/bulk"))); } @@ -46,7 +50,7 @@ public void testDefaultURL() throws URISyntaxException { public void testCustomURLNoPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com"); CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG); + HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/api/testImpressions/bulk"))); } @@ -54,7 +58,7 @@ public void testCustomURLNoPathNoBackslash() throws URISyntaxException { public void testCustomURLAppendingPath() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split/"); CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG); + HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/testImpressions/bulk"))); } @@ -62,7 +66,7 @@ public void testCustomURLAppendingPath() throws URISyntaxException { public void testCustomURLAppendingPathNoBackslash() throws URISyntaxException { URI rootTarget = URI.create("https://kubernetesturl.com/split"); CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG); + HttpImpressionsSender fetcher = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG, TELEMETRY_STORAGE); Assert.assertThat(fetcher.getTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/testImpressions/bulk"))); } @@ -74,7 +78,7 @@ public void testImpressionCountsEndpointOptimized() throws URISyntaxException, I CloseableHttpClient httpClient = TestHelper.mockHttpClient("", HttpStatus.SC_OK); // Send counters - HttpImpressionsSender sender = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.OPTIMIZED); + HttpImpressionsSender sender = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.OPTIMIZED, TELEMETRY_STORAGE); HashMap toSend = new HashMap<>(); toSend.put(new ImpressionCounter.Key("test1", 0), 4); toSend.put(new ImpressionCounter.Key("test2", 0), 5); @@ -104,7 +108,7 @@ public void testImpressionCountsEndpointDebug() throws URISyntaxException, IOExc CloseableHttpClient httpClient = TestHelper.mockHttpClient("", HttpStatus.SC_OK); // Send counters - HttpImpressionsSender sender = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG); + HttpImpressionsSender sender = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.DEBUG, TELEMETRY_STORAGE); HashMap toSend = new HashMap<>(); toSend.put(new ImpressionCounter.Key("test1", 0), 4); toSend.put(new ImpressionCounter.Key("test2", 0), 5); @@ -121,7 +125,7 @@ public void testImpressionBulksEndpoint() throws URISyntaxException, IOException // Setup response mock CloseableHttpClient httpClient = TestHelper.mockHttpClient("", HttpStatus.SC_OK); - HttpImpressionsSender sender = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.OPTIMIZED); + HttpImpressionsSender sender = HttpImpressionsSender.create(httpClient, rootTarget, ImpressionsManager.Mode.OPTIMIZED, TELEMETRY_STORAGE); // Send impressions List toSend = Arrays.asList(new TestImpressions("t1", Arrays.asList( @@ -152,7 +156,7 @@ public void testImpressionBulksEndpoint() throws URISyntaxException, IOException // Do the same flow for imrpessionsMode = debug CloseableHttpClient httpClientDebugMode = TestHelper.mockHttpClient("", HttpStatus.SC_OK); - sender = HttpImpressionsSender.create(httpClientDebugMode, rootTarget, ImpressionsManager.Mode.DEBUG); + sender = HttpImpressionsSender.create(httpClientDebugMode, rootTarget, ImpressionsManager.Mode.DEBUG, TELEMETRY_STORAGE); sender.postImpressionsBulk(toSend); captor = ArgumentCaptor.forClass(HttpUriRequest.class); verify(httpClientDebugMode).execute(captor.capture()); diff --git a/client/src/test/java/io/split/client/impressions/ImpressionCounterTest.java b/client/src/test/java/io/split/client/impressions/ImpressionCounterTest.java index 4d8737dde..b04c93eb6 100644 --- a/client/src/test/java/io/split/client/impressions/ImpressionCounterTest.java +++ b/client/src/test/java/io/split/client/impressions/ImpressionCounterTest.java @@ -4,10 +4,15 @@ import java.time.ZoneId; import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import static org.hamcrest.CoreMatchers.both; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.lessThan; import static org.hamcrest.core.IsEqual.equalTo; public class ImpressionCounterTest { @@ -101,4 +106,83 @@ public void manyConcurrentCalls() throws InterruptedException { assertThat(counted.get(new ImpressionCounter.Key("feature1", ImpressionUtils.truncateTimeframe(nextHourTimestamp))), is(equalTo(iterations * 3))); assertThat(counted.get(new ImpressionCounter.Key("feature2", ImpressionUtils.truncateTimeframe(nextHourTimestamp))), is(equalTo(iterations * 3))); } + + @Test + public void manyConcurrentCallsWithConcurrentPops() throws InterruptedException { + final int iterations = 10000000; + final long timestamp = makeTimestamp(2020, 9, 2, 10, 10, 12); + final long nextHourTimestamp = makeTimestamp(2020, 9, 2, 11, 10, 12); + ImpressionCounter counter = new ImpressionCounter(); + Thread t1 = new Thread(() -> { + int times = iterations; + while (times-- > 0) { + counter.inc("feature1", timestamp, 1); + counter.inc("feature2", timestamp, 1); + counter.inc("feature1", nextHourTimestamp, 2); + counter.inc("feature2", nextHourTimestamp, 2); + } + }); + Thread t2 = new Thread(() -> { + int times = iterations; + while (times-- > 0) { + counter.inc("feature1", timestamp, 2); + counter.inc("feature2", timestamp, 2); + counter.inc("feature1", nextHourTimestamp, 1); + counter.inc("feature2", nextHourTimestamp, 1); + } + }); + + // Pushing to this list will be done from a single thread. And querying will be done from the main one + // after all other threads have ended. No need for extra sync logic. + List> pops = new ArrayList<>(); + Thread t3 = new Thread(() -> { + try { + for (int i=10; i > 0; --i){ + Thread.sleep(1); + pops.add(counter.popAll()); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + + t1.setDaemon(true); t2.setDaemon(true); t2.setDaemon(true); + t1.start(); t2.start(); t3.start(); + t1.join(); t2.join(); t3.join(); + + // --- No other thread is running at this point. + + // Do an extra pop in case there's still some data in the counter + pops.add(counter.popAll()); + + Long feature1TSCount = pops.stream() + .reduce(0L, + (accum, next) -> accum + next.getOrDefault(new ImpressionCounter.Key("feature1", ImpressionUtils.truncateTimeframe(timestamp)), 0), + (x, y) -> x + y); + + Long feature1NextTSCount = pops.stream() + .reduce(0L, + (accum, next) -> accum + next.getOrDefault(new ImpressionCounter.Key("feature1", ImpressionUtils.truncateTimeframe(nextHourTimestamp)), 0), + (x, y) -> x + y); + + Long feature2TSCount = pops.stream() + .reduce(0L, + (accum, next) -> accum + next.getOrDefault(new ImpressionCounter.Key("feature2", ImpressionUtils.truncateTimeframe(timestamp)), 0), + (x, y) -> x + y); + + Long feature2NextTSCount = pops.stream() + .reduce(0L, + (accum, next) -> accum + next.getOrDefault(new ImpressionCounter.Key("feature2", ImpressionUtils.truncateTimeframe(nextHourTimestamp)), 0), + (x, y) -> x + y); + + + // Using lockless/atomic structures for higher performance at the cost of 0.001% margin error accepted in very high concurrency + Long lowerBound = (long) ((iterations * 3) * 0.99999); + Long upperBound = (long) ((iterations * 3) * 1.00001); + + assertThat(feature1TSCount, is(both(greaterThan(lowerBound)).and(lessThan(upperBound)))); + assertThat(feature1NextTSCount, is(both(greaterThan(lowerBound)).and(lessThan(upperBound)))); + assertThat(feature2TSCount, is(both(greaterThan(lowerBound)).and(lessThan(upperBound)))); + assertThat(feature2NextTSCount, is(both(greaterThan(lowerBound)).and(lessThan(upperBound)))); + } } diff --git a/client/src/test/java/io/split/client/impressions/ImpressionObserverTest.java b/client/src/test/java/io/split/client/impressions/ImpressionObserverTest.java index 564ce9e34..2fe3611e7 100644 --- a/client/src/test/java/io/split/client/impressions/ImpressionObserverTest.java +++ b/client/src/test/java/io/split/client/impressions/ImpressionObserverTest.java @@ -1,6 +1,7 @@ package io.split.client.impressions; import com.google.common.base.Strings; +import org.junit.Ignore; import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -118,6 +119,7 @@ private void caller(ImpressionObserver o, int count, ConcurrentLinkedQueue imps = new ConcurrentLinkedQueue<>(); diff --git a/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java b/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java index 4e812254e..a0fa1a7a2 100644 --- a/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java +++ b/client/src/test/java/io/split/client/impressions/ImpressionsManagerImplTest.java @@ -4,6 +4,10 @@ import io.split.client.dtos.KeyImpression; import io.split.client.dtos.TestImpressions; +import io.split.telemetry.domain.enums.ImpressionsDataTypeEnum; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -19,14 +23,19 @@ import static org.hamcrest.Matchers.*; import static org.junit.Assert.assertThat; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; /** * Created by patricioe on 6/20/16. */ @RunWith(MockitoJUnitRunner.class) public class ImpressionsManagerImplTest { + private static TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + + @Before + public void setUp() { + TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + } @Captor private ArgumentCaptor> impressionsCaptor; @@ -45,7 +54,7 @@ public void works() throws URISyntaxException { ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); - ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null); + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null, TELEMETRY_STORAGE); KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, null); KeyImpression ki2 = keyImpression("test1", "adil", "on", 2L, 1L); @@ -78,7 +87,7 @@ public void worksButDropsImpressions() throws URISyntaxException { ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); - ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null); + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null, TELEMETRY_STORAGE); // These 4 unique test name will cause 4 entries but we are caping at the first 3. KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, null); @@ -99,6 +108,7 @@ public void worksButDropsImpressions() throws URISyntaxException { List captured = impressionsCaptor.getValue(); assertThat(captured.size(), is(equalTo(3))); + Mockito.verify(TELEMETRY_STORAGE, times(1)).recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 1); } @Test @@ -112,7 +122,7 @@ public void works4ImpressionsInOneTest() throws URISyntaxException { ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); - ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null); + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null, TELEMETRY_STORAGE); // These 4 unique test name will cause 4 entries but we are caping at the first 3. KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, 1L); @@ -135,6 +145,7 @@ public void works4ImpressionsInOneTest() throws URISyntaxException { assertThat(captured.size(), is(equalTo(1))); assertThat(captured.get(0).keyImpressions.size(), is(equalTo(4))); assertThat(captured.get(0).keyImpressions.get(0), is(equalTo(ki1))); + Mockito.verify(TELEMETRY_STORAGE, times(4)).recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_QUEUED, 1); } @Test @@ -147,7 +158,7 @@ public void worksNoImpressions() throws URISyntaxException { .build(); ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); - ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null); + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null, TELEMETRY_STORAGE); // There are no impressions to post. @@ -168,7 +179,7 @@ public void alreadySeenImpressionsAreMarked() throws URISyntaxException { ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); - ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null); + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null, TELEMETRY_STORAGE); // These 4 unique test name will cause 4 entries but we are caping at the first 3. KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, 1L); @@ -229,7 +240,7 @@ public void testImpressionsOptimizedMode() throws URISyntaxException { ImpressionsSender senderMock = Mockito.mock(ImpressionsSender.class); - ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null); + ImpressionsManagerImpl treatmentLog = ImpressionsManagerImpl.instanceForTest(null, config, senderMock, null, TELEMETRY_STORAGE); // These 4 unique test name will cause 4 entries but we are caping at the first 3. KeyImpression ki1 = keyImpression("test1", "adil", "on", 1L, 1L); diff --git a/client/src/test/java/io/split/client/interceptors/AddSplitHeadersFilterTest.java b/client/src/test/java/io/split/client/interceptors/AddSplitHeadersFilterTest.java deleted file mode 100644 index 2c68b0335..000000000 --- a/client/src/test/java/io/split/client/interceptors/AddSplitHeadersFilterTest.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.split.client.interceptors; - -import org.apache.hc.core5.http.HttpException; -import org.apache.hc.core5.http.HttpRequest; -import org.apache.hc.core5.http.protocol.HttpContext; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Captor; -import org.mockito.Mockito; - -import org.mockito.runners.MockitoJUnitRunner; - -import java.io.IOException; -import java.util.List; - -import static org.hamcrest.Matchers.*; -import static org.hamcrest.core.IsEqual.equalTo; -import static org.junit.Assert.assertThat; - - -@RunWith(MockitoJUnitRunner.class) -public class AddSplitHeadersFilterTest { - - @Captor - private ArgumentCaptor headerNameCaptor; - - @Captor - private ArgumentCaptor headerValueCaptor; - - @Test - public void testHeadersWithIpAndHostname() throws IOException, HttpException { - AddSplitHeadersFilter filter = AddSplitHeadersFilter.instance("abc", true); - HttpRequest req = Mockito.mock(HttpRequest.class); - HttpContext ctx = Mockito.mock(HttpContext.class); - - filter.process(req, null, ctx); - Mockito.verify(req, Mockito.times(4)).addHeader(headerNameCaptor.capture(), headerValueCaptor.capture()); - - List headerNames = headerNameCaptor.getAllValues(); - List headerValues = headerValueCaptor.getAllValues(); - - assertThat(headerNames.size(), is(equalTo(4))); - assertThat(headerValues.size(), is(equalTo(4))); - - assertThat(headerNames, contains(AddSplitHeadersFilter.AUTHORIZATION_HEADER, - AddSplitHeadersFilter.CLIENT_VERSION, - AddSplitHeadersFilter.CLIENT_MACHINE_NAME_HEADER, - AddSplitHeadersFilter.CLIENT_MACHINE_IP_HEADER)); - } - - @Test - public void testHeadersWithoutIpAndHostname() throws IOException, HttpException { - AddSplitHeadersFilter filter = AddSplitHeadersFilter.instance("abc", false); - HttpRequest req = Mockito.mock(HttpRequest.class); - HttpContext ctx = Mockito.mock(HttpContext.class); - - filter.process(req, null, ctx); - Mockito.verify(req, Mockito.times(2)).addHeader(headerNameCaptor.capture(), headerValueCaptor.capture()); - - List headerNames = headerNameCaptor.getAllValues(); - List headerValues = headerValueCaptor.getAllValues(); - - assertThat(headerNames.size(), is(equalTo(2))); - assertThat(headerValues.size(), is(equalTo(2))); - - assertThat(headerNames, contains(AddSplitHeadersFilter.AUTHORIZATION_HEADER, - AddSplitHeadersFilter.CLIENT_VERSION)); - - assertThat(headerNames, not(contains(AddSplitHeadersFilter.CLIENT_MACHINE_NAME_HEADER, - AddSplitHeadersFilter.CLIENT_MACHINE_IP_HEADER))); - } -} diff --git a/client/src/test/java/io/split/client/interceptors/AuthorizationInterceptorFilterTest.java b/client/src/test/java/io/split/client/interceptors/AuthorizationInterceptorFilterTest.java new file mode 100644 index 000000000..1f10a90dd --- /dev/null +++ b/client/src/test/java/io/split/client/interceptors/AuthorizationInterceptorFilterTest.java @@ -0,0 +1,51 @@ +package io.split.client.interceptors; + +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.*; + +@RunWith(MockitoJUnitRunner.class) +public class AuthorizationInterceptorFilterTest { + + @Captor + private ArgumentCaptor headerNameCaptor; + + @Captor + private ArgumentCaptor headerValueCaptor; + + @Test + public void authorizationInterceptorWithValue() throws IOException, HttpException { + AuthorizationInterceptorFilter filter = AuthorizationInterceptorFilter.instance("api-token-test"); + HttpRequest req = Mockito.mock(HttpRequest.class); + HttpContext ctx = Mockito.mock(HttpContext.class); + + filter.process(req, null, ctx); + Mockito.verify(req, Mockito.times(1)).addHeader(headerNameCaptor.capture(), headerValueCaptor.capture()); + + List headerNames = headerNameCaptor.getAllValues(); + List headerValues = headerValueCaptor.getAllValues(); + + assertEquals(1, headerNames.size()); + assertEquals(1, headerValues.size()); + + assertThat(headerNames, contains(AuthorizationInterceptorFilter.AUTHORIZATION_HEADER)); + assertThat(headerValues, contains("Bearer api-token-test")); + } + + @Test(expected = Exception.class) + public void authorizationInterceptorWithoutValue() { + AuthorizationInterceptorFilter filter = AuthorizationInterceptorFilter.instance(null); + } +} diff --git a/client/src/test/java/io/split/client/interceptors/ClientKeyInterceptorFilterTest.java b/client/src/test/java/io/split/client/interceptors/ClientKeyInterceptorFilterTest.java new file mode 100644 index 000000000..56121882b --- /dev/null +++ b/client/src/test/java/io/split/client/interceptors/ClientKeyInterceptorFilterTest.java @@ -0,0 +1,46 @@ +package io.split.client.interceptors; + +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class ClientKeyInterceptorFilterTest { + @Captor + private ArgumentCaptor headerNameCaptor; + + @Captor + private ArgumentCaptor headerValueCaptor; + + @Test + public void ClientKeyInterceptorFilter() throws IOException, HttpException { + ClientKeyInterceptorFilter filter = ClientKeyInterceptorFilter.instance("api-token-test-1234"); + HttpRequest req = Mockito.mock(HttpRequest.class); + HttpContext ctx = Mockito.mock(HttpContext.class); + + filter.process(req, null, ctx); + Mockito.verify(req, Mockito.times(1)).addHeader(headerNameCaptor.capture(), headerValueCaptor.capture()); + + List headerNames = headerNameCaptor.getAllValues(); + List headerValues = headerValueCaptor.getAllValues(); + + assertEquals(1, headerNames.size()); + assertEquals(1, headerValues.size()); + + assertThat(headerNames, contains(ClientKeyInterceptorFilter.CLIENT_KEY)); + assertThat(headerValues, contains("1234")); + } +} diff --git a/client/src/test/java/io/split/client/interceptors/SdkMetadataInterceptorFilterTest.java b/client/src/test/java/io/split/client/interceptors/SdkMetadataInterceptorFilterTest.java new file mode 100644 index 000000000..6f1336516 --- /dev/null +++ b/client/src/test/java/io/split/client/interceptors/SdkMetadataInterceptorFilterTest.java @@ -0,0 +1,65 @@ +package io.split.client.interceptors; + +import org.apache.hc.core5.http.HttpException; +import org.apache.hc.core5.http.HttpRequest; +import org.apache.hc.core5.http.protocol.HttpContext; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Mockito; +import org.mockito.runners.MockitoJUnitRunner; + +import java.io.IOException; +import java.util.List; + +import static org.hamcrest.Matchers.contains; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThat; + +@RunWith(MockitoJUnitRunner.class) +public class SdkMetadataInterceptorFilterTest { + @Captor + private ArgumentCaptor headerNameCaptor; + + @Captor + private ArgumentCaptor headerValueCaptor; + + @Test + public void sdkMetadataWithIpEnabled() throws IOException, HttpException { + SdkMetadataInterceptorFilter filter = SdkMetadataInterceptorFilter.instance(true, "sdk-version-1.2.3"); + HttpRequest req = Mockito.mock(HttpRequest.class); + HttpContext ctx = Mockito.mock(HttpContext.class); + + filter.process(req, null, ctx); + Mockito.verify(req, Mockito.times(3)).addHeader(headerNameCaptor.capture(), headerValueCaptor.capture()); + + List headerNames = headerNameCaptor.getAllValues(); + List headerValues = headerValueCaptor.getAllValues(); + + assertEquals(3, headerNames.size()); + assertEquals(3, headerValues.size()); + + assertThat(headerNames, contains(SdkMetadataInterceptorFilter.CLIENT_VERSION, + SdkMetadataInterceptorFilter.CLIENT_MACHINE_NAME_HEADER, + SdkMetadataInterceptorFilter.CLIENT_MACHINE_IP_HEADER)); + } + + @Test + public void sdkMetadataWithIpDisabled() throws IOException, HttpException { + SdkMetadataInterceptorFilter filter = SdkMetadataInterceptorFilter.instance(false, "sdk-version-1.2.3"); + HttpRequest req = Mockito.mock(HttpRequest.class); + HttpContext ctx = Mockito.mock(HttpContext.class); + + filter.process(req, null, ctx); + Mockito.verify(req, Mockito.times(1)).addHeader(headerNameCaptor.capture(), headerValueCaptor.capture()); + + List headerNames = headerNameCaptor.getAllValues(); + List headerValues = headerValueCaptor.getAllValues(); + + assertEquals(1, headerNames.size()); + assertEquals(1, headerValues.size()); + + assertThat(headerNames, contains(SdkMetadataInterceptorFilter.CLIENT_VERSION)); + } +} diff --git a/client/src/test/java/io/split/client/metrics/BinarySearchLatencyTrackerTest.java b/client/src/test/java/io/split/client/metrics/BinarySearchLatencyTrackerTest.java deleted file mode 100644 index c04fa5b49..000000000 --- a/client/src/test/java/io/split/client/metrics/BinarySearchLatencyTrackerTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package io.split.client.metrics; - -import org.junit.Before; -import org.junit.Test; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.*; - -public class BinarySearchLatencyTrackerTest { - - BinarySearchLatencyTracker tracker; - - @Before - public void before() { - tracker = new BinarySearchLatencyTracker(); - } - - /** - * Latencies of <=1 millis or <= 1000 micros correspond to the first bucket (index 0) - */ - @Test - public void testLessThanFirstBucket() { - - tracker.addLatencyMicros(750); - tracker.addLatencyMicros(450); - assertThat(tracker.getLatency(0), is(equalTo(2L))); - - tracker.addLatencyMillis(0); - assertThat(tracker.getLatency(0), is(equalTo(3L))); - } - - /** - * Latencies of 1 millis or <= 1000 micros correspond to the first bucket (index 0) - */ - @Test - public void testFirstBucket() { - - tracker.addLatencyMicros(1000); - assertThat(tracker.getLatency(0), is(equalTo(1L))); - - tracker.addLatencyMillis(1); - assertThat(tracker.getLatency(0), is(equalTo(2L))); - } - - /** - * Latencies of 7481 millis or 7481828 micros correspond to the last bucket (index 22) - */ - @Test - public void testLastBucket() { - - tracker.addLatencyMicros(7481828); - assertThat(tracker.getLatency(22), is(equalTo(1L))); - - tracker.addLatencyMillis(7481); - assertThat(tracker.getLatency(22), is(equalTo(2L))); - } - - /** - * Latencies of more than 7481 millis or 7481828 micros correspond to the last bucket (index 22) - */ - @Test - public void testGreaterThanLastBucket() { - - tracker.addLatencyMicros(7481830); - assertThat(tracker.getLatency(22), is(equalTo(1L))); - - tracker.addLatencyMicros(7999999); - assertThat(tracker.getLatency(22), is(equalTo(2L))); - - tracker.addLatencyMillis(7482); - assertThat(tracker.getLatency(22), is(equalTo(3L))); - - tracker.addLatencyMillis(8000); - assertThat(tracker.getLatency(22), is(equalTo(4L))); - } - - /** - * Latencies between 11,392 and 17,086 are in the 8th bucket. - */ - @Test - public void test8ThBucket() { - - tracker.addLatencyMicros(11392); - assertThat(tracker.getLatency(7), is(equalTo(1L))); - - tracker.addLatencyMicros(17086); - assertThat(tracker.getLatency(7), is(equalTo(2L))); - - } - -} \ No newline at end of file diff --git a/client/src/test/java/io/split/client/metrics/CachedMetricsTest.java b/client/src/test/java/io/split/client/metrics/CachedMetricsTest.java deleted file mode 100644 index 57dd8a181..000000000 --- a/client/src/test/java/io/split/client/metrics/CachedMetricsTest.java +++ /dev/null @@ -1,77 +0,0 @@ -package io.split.client.metrics; - -import com.google.common.collect.Lists; -import io.split.client.dtos.Counter; -import io.split.client.dtos.Latency; -import org.junit.Test; - -import java.util.List; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -/** - * Created by adilaijaz on 9/23/15. - */ -public class CachedMetricsTest { - - private static final class MyDTOMetrics implements DTOMetrics { - - private List latencies = Lists.newArrayList(); - private List counters = Lists.newArrayList(); - - @Override - public void time(Latency dto) { - latencies.add(dto); - } - - @Override - public void count(Counter dto) { - counters.add(dto); - } - } - - @Test - public void count() { - MyDTOMetrics metrics = new MyDTOMetrics(); - - CachedMetrics cachedMetrics = new CachedMetrics(metrics, 2); - - cachedMetrics.count("foo", 4); - cachedMetrics.count("foo", 5); - cachedMetrics.count("foo", 6); - cachedMetrics.count("foo", 7); - cachedMetrics.count("foo", 8); - - Counter counter = new Counter(); - counter.name = "foo"; - counter.delta = 9L; - - assertThat(metrics.counters.size(), is(equalTo(2))); - assertThat(metrics.counters.get(0).name, is(equalTo("foo"))); - assertThat(metrics.counters.get(0).delta, is(equalTo(9L))); - - assertThat(metrics.counters.get(1).name, is(equalTo("foo"))); - assertThat(metrics.counters.get(1).delta, is(equalTo(13L))); - } - - @Test - public void latency() throws Exception { - MyDTOMetrics delegate = new MyDTOMetrics(); - CachedMetrics cachedMetrics = new CachedMetrics(delegate, 15L); - - cachedMetrics.time("foo", 4); - cachedMetrics.time("foo", 5); - cachedMetrics.time("foo", 6); - cachedMetrics.time("foo", 7); - Thread.sleep(30); - cachedMetrics.time("foo", 8); - - List latencies = Lists.newArrayList(0L, 0L, 0L, 0L, 2L, 2L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L); - - assertThat(delegate.latencies.get(0).name, is(equalTo("foo"))); - assertThat(delegate.latencies.get(0).latencies, is(equalTo(latencies))); - } - -} diff --git a/client/src/test/java/io/split/client/metrics/HttpMetricsTest.java b/client/src/test/java/io/split/client/metrics/HttpMetricsTest.java deleted file mode 100644 index 71ade9dbe..000000000 --- a/client/src/test/java/io/split/client/metrics/HttpMetricsTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package io.split.client.metrics; - -import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; -import org.apache.hc.client5.http.impl.classic.HttpClients; -import org.hamcrest.Matchers; -import org.junit.Assert; -import org.junit.Test; - -import java.net.URI; -import java.net.URISyntaxException; - -public class HttpMetricsTest { - @Test - public void testDefaultURL() throws URISyntaxException { - URI rootTarget = URI.create("https://api.split.io"); - CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpMetrics fetcher = HttpMetrics.create(httpClient, rootTarget); - Assert.assertThat(fetcher.getTimeTarget().toString(), Matchers.is(Matchers.equalTo("https://api.split.io/api/metrics/time"))); - Assert.assertThat(fetcher.getCounterTarget().toString(), Matchers.is(Matchers.equalTo("https://api.split.io/api/metrics/counter"))); - } - - @Test - public void testCustomURLNoPathNoBackslash() throws URISyntaxException { - URI rootTarget = URI.create("https://kubernetesturl.com"); - CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpMetrics fetcher = HttpMetrics.create(httpClient, rootTarget); - Assert.assertThat(fetcher.getTimeTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/api/metrics/time"))); - Assert.assertThat(fetcher.getCounterTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/api/metrics/counter"))); - } - - @Test - public void testCustomURLAppendingPath() throws URISyntaxException { - URI rootTarget = URI.create("https://kubernetesturl.com/split/"); - CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpMetrics fetcher = HttpMetrics.create(httpClient, rootTarget); - Assert.assertThat(fetcher.getTimeTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/metrics/time"))); - Assert.assertThat(fetcher.getCounterTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/metrics/counter"))); - } - - @Test - public void testCustomURLAppendingPathNoBackslash() throws URISyntaxException { - URI rootTarget = URI.create("https://kubernetesturl.com/split"); - CloseableHttpClient httpClient = HttpClients.custom().build(); - HttpMetrics fetcher = HttpMetrics.create(httpClient, rootTarget); - Assert.assertThat(fetcher.getTimeTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/metrics/time"))); - Assert.assertThat(fetcher.getCounterTarget().toString(), Matchers.is(Matchers.equalTo("https://kubernetesturl.com/split/api/metrics/counter"))); - } -} diff --git a/client/src/test/java/io/split/client/metrics/LogarithmicSearchLatencyTrackerTest.java b/client/src/test/java/io/split/client/metrics/LogarithmicSearchLatencyTrackerTest.java deleted file mode 100644 index fedcec0a9..000000000 --- a/client/src/test/java/io/split/client/metrics/LogarithmicSearchLatencyTrackerTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.split.client.metrics; - -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; - -import static org.hamcrest.Matchers.equalTo; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -@Ignore -public class LogarithmicSearchLatencyTrackerTest { - - LogarithmicSearchLatencyTracker tracker; - - @Before - public void before() { - tracker = new LogarithmicSearchLatencyTracker(); - } - - /** - * Latencies of <=1 millis or <= 1000 micros correspond to the first bucket (index 0) - */ - @Test - public void testLessThanFirstBucket() { - - tracker.addLatencyMicros(750); - tracker.addLatencyMicros(450); - assertThat(tracker.getLatency(0), is(equalTo(2L))); - - tracker.addLatencyMillis(0); - assertThat(tracker.getLatency(0), is(equalTo(3L))); - } - - /** - * Latencies of 1 millis or <= 1000 micros correspond to the first bucket (index 0) - */ - @Test - public void testFirstBucket() { - - tracker.addLatencyMicros(1000); - assertThat(tracker.getLatency(0), is(equalTo(1L))); - - tracker.addLatencyMillis(1); - assertThat(tracker.getLatency(0), is(equalTo(2L))); - } - - /** - * Latencies of 7481 millis or 7481828 micros correspond to the last bucket (index 22) - */ - @Test - public void testLastBucket() { - - tracker.addLatencyMicros(7481828); - assertThat(tracker.getLatency(22), is(equalTo(1L))); - - tracker.addLatencyMillis(7481); - assertThat(tracker.getLatency(22), is(equalTo(2L))); - } - - /** - * Latencies of more than 7481 millis or 7481828 micros correspond to the last bucket (index 22) - */ - @Test - public void testGreaterThanLastBucket() { - - tracker.addLatencyMicros(7481830); - assertThat(tracker.getLatency(22), is(equalTo(1L))); - - tracker.addLatencyMicros(7999999); - assertThat(tracker.getLatency(22), is(equalTo(2L))); - - tracker.addLatencyMillis(7482); - assertThat(tracker.getLatency(22), is(equalTo(3L))); - - tracker.addLatencyMillis(8000); - assertThat(tracker.getLatency(22), is(equalTo(4L))); - } - - /** - * Latencies between 11,392 and 17,086 are in the 8th bucket. - */ - @Test - public void test8ThBucket() { - - tracker.addLatencyMicros(11392); - assertThat(tracker.getLatency(7), is(equalTo(1L))); - - tracker.addLatencyMicros(17086); - assertThat(tracker.getLatency(7), is(equalTo(2L))); - - tracker.addLatencyMillis(18); - assertThat(tracker.getLatency(7), is(equalTo(3L))); - } - - /** - * Latencies between 656,842 and 985,261 are in the 18th bucket. - */ - @Test - public void test17ThBucket() { - - tracker.addLatencyMicros(656842); - assertThat(tracker.getLatency(17), is(equalTo(1L))); - - tracker.addLatencyMicros(985261); - assertThat(tracker.getLatency(17), is(equalTo(2L))); - - tracker.addLatencyMillis(985); - assertThat(tracker.getLatency(17), is(equalTo(3L))); - } - -} \ No newline at end of file diff --git a/client/src/test/java/io/split/engine/common/FastlyHeadersCaptorTest.java b/client/src/test/java/io/split/engine/common/FastlyHeadersCaptorTest.java new file mode 100644 index 000000000..7fd4a4106 --- /dev/null +++ b/client/src/test/java/io/split/engine/common/FastlyHeadersCaptorTest.java @@ -0,0 +1,62 @@ +package io.split.engine.common; + +import org.junit.Test; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; + +public class FastlyHeadersCaptorTest { + + @Test + public void filterWorks() { + FastlyHeadersCaptor captor = new FastlyHeadersCaptor(); + captor.handle(Stream.of(new String[][] { + {"Fastly-Debug-Path", "something"}, + {"Fastly-Debug-TTL", "something"}, + {"Fastly-Debug-Digest", "something"}, + {"X-Served-By", "something"}, + {"X-Cache", "something"}, + {"X-Cache-Hits", "something"}, + {"X-Timer", "something"}, + {"Surrogate-Key", "something"}, + {"ETag", "something"}, + {"Cache-Control", "something"}, + {"X-Request-ID", "something"}, + {"Last-Modified", "something"}, + {"NON_IMPORTANT_1", "something"}, + {"ANOTHER_NON_IMPORTANT", "something"} + }).collect(Collectors.toMap(d -> d[0], d -> d[1]))); + + assertEquals(captor.get().size(), 1); + assertEquals(captor.get().get(0).size(), 12); + assertFalse(captor.get().get(0).containsKey("NON_IMPORTANT_1")); + assertFalse(captor.get().get(0).containsKey("ANOTHER_NON_IMPORTANT")); + } + + @Test + public void orderIsPreserved() { + FastlyHeadersCaptor captor = new FastlyHeadersCaptor(); + captor.handle(Stream.of(new String[][]{ + {"Fastly-Debug-Path", "first"}, + }).collect(Collectors.toMap(d -> d[0], d -> d[1]))); + + captor.handle(Stream.of(new String[][]{ + {"Fastly-Debug-Path", "second"}, + }).collect(Collectors.toMap(d -> d[0], d -> d[1]))); + + captor.handle(Stream.of(new String[][]{ + {"Fastly-Debug-Path", "third"}, + }).collect(Collectors.toMap(d -> d[0], d -> d[1]))); + + assertEquals(captor.get().size(), 3); + assertEquals(captor.get().get(0).size(), 1); + assertEquals(captor.get().get(1).size(), 1); + assertEquals(captor.get().get(2).size(), 1); + assertEquals(captor.get().get(0).get("Fastly-Debug-Path"), "first"); + assertEquals(captor.get().get(1).get("Fastly-Debug-Path"), "second"); + assertEquals(captor.get().get(2).get("Fastly-Debug-Path"), "third"); + } +} diff --git a/client/src/test/java/io/split/engine/common/FetcherOptionsTest.java b/client/src/test/java/io/split/engine/common/FetcherOptionsTest.java new file mode 100644 index 000000000..25b2ea0fd --- /dev/null +++ b/client/src/test/java/io/split/engine/common/FetcherOptionsTest.java @@ -0,0 +1,49 @@ +package io.split.engine.common; + +import org.junit.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import static org.junit.Assert.assertEquals; + +public class FetcherOptionsTest { + + @Test + public void optionsPropagatedOk() { + final boolean[] called = {false}; + Function, Void> func = new Function, Void>() { + @Override + public Void apply(Map unused) { + called[0] = true; + return null; + } + }; + + FetchOptions options = new FetchOptions.Builder() + .cacheControlHeaders(true) + .fastlyDebugHeader(true) + .responseHeadersCallback(func) + .targetChangeNumber(123) + .build(); + + assertEquals(options.cacheControlHeadersEnabled(), true); + assertEquals(options.fastlyDebugHeaderEnabled(), true); + assertEquals(options.targetCN(), 123); + options.handleResponseHeaders(new HashMap<>()); + assertEquals(called[0], true); + } + + @Test + public void nullHandlerDoesNotExplode() { + + FetchOptions options = new FetchOptions.Builder() + .cacheControlHeaders(true) + .fastlyDebugHeader(true) + .responseHeadersCallback(null) + .build(); + + options.handleResponseHeaders(new HashMap<>()); + } +} 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 4662ddab2..d93ccbae5 100644 --- a/client/src/test/java/io/split/engine/common/PushManagerTest.java +++ b/client/src/test/java/io/split/engine/common/PushManagerTest.java @@ -8,6 +8,9 @@ import io.split.engine.sse.dtos.AuthenticationResponse; import io.split.engine.sse.workers.SegmentsWorkerImp; import io.split.engine.sse.workers.SplitsWorker; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -20,6 +23,7 @@ public class PushManagerTest { private Backoff _backoff; private PushManager _pushManager; private PushStatusTracker _pushStatusTracker; + private TelemetryStorage _telemetryStorage; @Before public void setUp() { @@ -27,11 +31,12 @@ public void setUp() { _eventSourceClient = Mockito.mock(EventSourceClient.class); _backoff = Mockito.mock(Backoff.class); _pushStatusTracker = Mockito.mock(PushStatusTrackerImp.class); + _telemetryStorage = new InMemoryTelemetryStorage(); _pushManager = new PushManagerImp(_authApiClient, _eventSourceClient, Mockito.mock(SplitsWorker.class), Mockito.mock(SegmentsWorkerImp.class), - _pushStatusTracker); + _pushStatusTracker, _telemetryStorage); } @Test @@ -58,6 +63,7 @@ public void startWithPushEnabledShouldConnect() throws InterruptedException { Mockito.verify(_pushStatusTracker, Mockito.times(0)).handleSseStatus(SSEClient.StatusMessage.RETRYABLE_ERROR); Mockito.verify(_pushStatusTracker, Mockito.times(0)).forcePushDisable(); + Assert.assertEquals(1, _telemetryStorage.popStreamingEvents().size()); } @Test diff --git a/client/src/test/java/io/split/engine/common/SyncManagerTest.java b/client/src/test/java/io/split/engine/common/SyncManagerTest.java index 551ebd7f3..466fe38a8 100644 --- a/client/src/test/java/io/split/engine/common/SyncManagerTest.java +++ b/client/src/test/java/io/split/engine/common/SyncManagerTest.java @@ -1,5 +1,10 @@ package io.split.engine.common; +import io.split.client.SplitClientConfig; +import io.split.engine.SDKReadinessGates; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import io.split.telemetry.synchronizer.TelemetrySynchronizer; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; @@ -10,49 +15,71 @@ public class SyncManagerTest { private static final int BACKOFF_BASE = 1; private Synchronizer _synchronizer; private PushManager _pushManager; + private SDKReadinessGates _gates; @Before public void setUp() { _synchronizer = Mockito.mock(Synchronizer.class); _pushManager = Mockito.mock(PushManager.class); + _gates = Mockito.mock(SDKReadinessGates.class); } @Test - public void startWithStreamingFalseShouldStartPolling() { - SyncManagerImp syncManager = new SyncManagerImp(false, _synchronizer, _pushManager, new LinkedBlockingQueue<>(), BACKOFF_BASE); + public void startWithStreamingFalseShouldStartPolling() throws InterruptedException { + TelemetryStorage telemetryStorage = Mockito.mock(TelemetryStorage.class); + _gates.sdkInternalReady(); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + Mockito.when(_synchronizer.syncAll()).thenReturn(true); + SyncManagerImp syncManager = new SyncManagerImp(false, _synchronizer, _pushManager, new LinkedBlockingQueue<>(), BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); syncManager.start(); + Thread.sleep(1000); Mockito.verify(_synchronizer, Mockito.times(1)).startPeriodicFetching(); - Mockito.verify(_synchronizer, Mockito.times(0)).syncAll(); + Mockito.verify(_synchronizer, Mockito.times(1)).syncAll(); Mockito.verify(_pushManager, Mockito.times(0)).start(); } @Test - public void startWithStreamingTrueShouldStartSyncAll() { - SyncManager sm = new SyncManagerImp(true, _synchronizer, _pushManager, new LinkedBlockingQueue<>(), BACKOFF_BASE); + public void startWithStreamingTrueShouldStartSyncAll() throws InterruptedException { + TelemetryStorage telemetryStorage = Mockito.mock(TelemetryStorage.class); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + Mockito.when(_synchronizer.syncAll()).thenReturn(true); + SyncManager sm = new SyncManagerImp(true, _synchronizer, _pushManager, new LinkedBlockingQueue<>(), BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); sm.start(); + Thread.sleep(1000); Mockito.verify(_synchronizer, Mockito.times(0)).startPeriodicFetching(); Mockito.verify(_synchronizer, Mockito.times(1)).syncAll(); Mockito.verify(_pushManager, Mockito.times(1)).start(); + Mockito.verify(telemetryStorage, Mockito.times(1)).recordStreamingEvents(Mockito.any()); } @Test public void onStreamingAvailable() throws InterruptedException { - LinkedBlockingQueue messsages = new LinkedBlockingQueue<>(); - SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE); + TelemetryStorage telemetryStorage = Mockito.mock(TelemetryStorage.class); + LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + + SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messages, BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); Thread t = new Thread(syncManager::incomingPushStatusHandler); t.start(); - messsages.offer(PushManager.Status.STREAMING_READY); + messages.offer(PushManager.Status.STREAMING_READY); Thread.sleep(500); Mockito.verify(_synchronizer, Mockito.times(1)).stopPeriodicFetching(); Mockito.verify(_synchronizer, Mockito.times(1)).syncAll(); Mockito.verify(_pushManager, Mockito.times(1)).startWorkers(); + Mockito.verify(telemetryStorage, Mockito.times(1)).recordStreamingEvents(Mockito.any()); t.interrupt(); } @Test public void onStreamingDisabled() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messsages = new LinkedBlockingQueue<>(); - SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); Thread t = new Thread(syncManager::incomingPushStatusHandler); t.start(); messsages.offer(PushManager.Status.STREAMING_DOWN); @@ -65,8 +92,11 @@ public void onStreamingDisabled() throws InterruptedException { @Test public void onStreamingShutdown() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messsages = new LinkedBlockingQueue<>(); - SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); Thread t = new Thread(syncManager::incomingPushStatusHandler); t.start(); messsages.offer(PushManager.Status.STREAMING_OFF); @@ -77,8 +107,11 @@ public void onStreamingShutdown() throws InterruptedException { @Test public void onConnected() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messsages = new LinkedBlockingQueue<>(); - SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); Thread t = new Thread(syncManager::incomingPushStatusHandler); t.start(); messsages.offer(PushManager.Status.STREAMING_READY); @@ -90,8 +123,11 @@ public void onConnected() throws InterruptedException { @Test public void onDisconnect() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messsages = new LinkedBlockingQueue<>(); - SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); Thread t = new Thread(syncManager::incomingPushStatusHandler); t.start(); messsages.offer(PushManager.Status.STREAMING_OFF); @@ -102,8 +138,12 @@ public void onDisconnect() throws InterruptedException { @Test public void onDisconnectAndReconnect() throws InterruptedException { // Check with mauro. reconnect should call pushManager.start again, right? + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messsages = new LinkedBlockingQueue<>(); - SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + Mockito.when(_synchronizer.syncAll()).thenReturn(true); + SyncManagerImp syncManager = new SyncManagerImp(true, _synchronizer, _pushManager, messsages, BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); syncManager.start(); messsages.offer(PushManager.Status.STREAMING_BACKOFF); Thread.sleep(1200); @@ -111,4 +151,20 @@ public void onDisconnectAndReconnect() throws InterruptedException { // Check wi Mockito.verify(_synchronizer, Mockito.times(1)).syncAll(); Mockito.verify(_pushManager, Mockito.times(2)).start(); } + + @Test + public void syncAllRetryThenShouldStartPolling() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySynchronizer.class); + SplitClientConfig config = Mockito.mock(SplitClientConfig.class); + Mockito.when(_synchronizer.syncAll()).thenReturn(false).thenReturn(true); + SyncManagerImp syncManager = new SyncManagerImp(false, _synchronizer, _pushManager, new LinkedBlockingQueue<>(), BACKOFF_BASE, _gates, telemetryStorage, telemetrySynchronizer, config); + syncManager.start(); + Thread.sleep(2000); + Mockito.verify(_synchronizer, Mockito.times(1)).startPeriodicFetching(); + Mockito.verify(_synchronizer, Mockito.times(2)).syncAll(); + Mockito.verify(_pushManager, Mockito.times(0)).start(); + Mockito.verify(_gates, Mockito.times(1)).sdkInternalReady(); + Mockito.verify(telemetrySynchronizer, Mockito.times(1)).synchronizeConfig(Mockito.anyObject(), Mockito.anyLong(), Mockito.anyObject(), Mockito.anyObject()); + } } 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 5bff9d000..88bfdb903 100644 --- a/client/src/test/java/io/split/engine/common/SynchronizerTest.java +++ b/client/src/test/java/io/split/engine/common/SynchronizerTest.java @@ -1,15 +1,26 @@ package io.split.engine.common; +import io.split.cache.InMemoryCacheImp; import io.split.cache.SegmentCache; import io.split.cache.SplitCache; +import io.split.engine.SDKReadinessGates; import io.split.engine.experiments.SplitFetcherImp; import io.split.engine.experiments.SplitSynchronizationTask; import io.split.engine.segments.SegmentFetcher; import io.split.engine.segments.SegmentSynchronizationTask; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.mockito.Mockito.when; + public class SynchronizerTest { private SplitSynchronizationTask _refreshableSplitFetcherTask; private SegmentSynchronizationTask _segmentFetcher; @@ -17,6 +28,7 @@ public class SynchronizerTest { private SplitCache _splitCache; private Synchronizer _synchronizer; private SegmentCache _segmentCache; + private SDKReadinessGates _gates; @Before public void beforeMethod() { @@ -25,17 +37,20 @@ public void beforeMethod() { _splitFetcher = Mockito.mock(SplitFetcherImp.class); _splitCache = Mockito.mock(SplitCache.class); _segmentCache = Mockito.mock(SegmentCache.class); + _gates = Mockito.mock(SDKReadinessGates.class); - _synchronizer = new SynchronizerImp(_refreshableSplitFetcherTask, _splitFetcher, _segmentFetcher, _splitCache, _segmentCache); + _synchronizer = new SynchronizerImp(_refreshableSplitFetcherTask, _splitFetcher, _segmentFetcher, _splitCache, _segmentCache, 50, 10, 5, false, _gates); } @Test public void syncAll() throws InterruptedException { + Mockito.when(_splitFetcher.fetchAll(Mockito.anyObject())).thenReturn(true); + Mockito.when(_segmentFetcher.fetchAllSynchronous()).thenReturn(true); _synchronizer.syncAll(); - Thread.sleep(100); - Mockito.verify(_splitFetcher, Mockito.times(1)).fetchAll(true); - Mockito.verify(_segmentFetcher, Mockito.times(1)).fetchAll(true); + Thread.sleep(1000); + Mockito.verify(_splitFetcher, Mockito.times(1)).fetchAll(Mockito.anyObject()); + Mockito.verify(_segmentFetcher, Mockito.times(1)).fetchAllSynchronous(); } @Test @@ -56,7 +71,7 @@ public void stopPeriodicFetching() { @Test public void streamingRetryOnSplit() { - Mockito.when(_splitCache.getChangeNumber()).thenReturn(0l).thenReturn(0l).thenReturn(1l); + when(_splitCache.getChangeNumber()).thenReturn(0l).thenReturn(0l).thenReturn(1l); _synchronizer.refreshSplits(1l); Mockito.verify(_splitCache, Mockito.times(3)).getChangeNumber(); @@ -65,11 +80,164 @@ public void streamingRetryOnSplit() { @Test public void streamingRetryOnSegment() { SegmentFetcher fetcher = Mockito.mock(SegmentFetcher.class); - Mockito.when(_segmentFetcher.getFetcher(Mockito.anyString())).thenReturn(fetcher); - Mockito.when(_segmentCache.getChangeNumber(Mockito.anyString())).thenReturn(0l).thenReturn(0l).thenReturn(1l); + when(_segmentFetcher.getFetcher(Mockito.anyString())).thenReturn(fetcher); + when(_segmentCache.getChangeNumber(Mockito.anyString())).thenReturn(0l).thenReturn(0l).thenReturn(1l); _synchronizer.refreshSegment("Segment",1l); Mockito.verify(_segmentCache, Mockito.times(3)).getChangeNumber(Mockito.anyString()); } + @Test + public void testCDNBypassIsRequestedAfterNFailures() throws NoSuchFieldException, IllegalAccessException { + + SplitCache cache = new InMemoryCacheImp(); + Synchronizer imp = new SynchronizerImp(_refreshableSplitFetcherTask, + _splitFetcher, + _segmentFetcher, + cache, + _segmentCache, + 50, + 3, + 1, + true, + Mockito.mock(SDKReadinessGates.class)); + + ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class); + AtomicInteger calls = new AtomicInteger(); + Mockito.doAnswer(invocationOnMock -> { + calls.getAndIncrement(); + switch (calls.get()) { + case 4: cache.setChangeNumber(123); + } + return null; + }).when(_splitFetcher).forceRefresh(optionsCaptor.capture()); + + imp.refreshSplits(123); + + List options = optionsCaptor.getAllValues(); + Assert.assertEquals(options.size(), 4); + Assert.assertFalse(options.get(0).hasCustomCN()); + Assert.assertFalse(options.get(1).hasCustomCN()); + Assert.assertFalse(options.get(2).hasCustomCN()); + Assert.assertTrue(options.get(3).hasCustomCN()); + } + + @Test + public void testCDNBypassRequestLimitAndBackoff() throws NoSuchFieldException, IllegalAccessException { + + SplitCache cache = new InMemoryCacheImp(); + Synchronizer imp = new SynchronizerImp(_refreshableSplitFetcherTask, + _splitFetcher, + _segmentFetcher, + cache, + _segmentCache, + 50, + 3, + 1, + true, + Mockito.mock(SDKReadinessGates.class)); + + ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class); + AtomicInteger calls = new AtomicInteger(); + Mockito.doAnswer(invocationOnMock -> { + calls.getAndIncrement(); + switch (calls.get()) { + case 14: Assert.assertTrue(false); // should never get here + } + return null; + }).when(_splitFetcher).forceRefresh(optionsCaptor.capture()); + + // Before executing, we'll update the backoff via reflection, to avoid waiting minutes for the test to run. + Field backoffBase = SynchronizerImp.class.getDeclaredField("ON_DEMAND_FETCH_BACKOFF_BASE_MS"); + backoffBase.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(backoffBase, backoffBase.getModifiers() & ~Modifier.FINAL); + backoffBase.set(imp, 1); // 1ms + + long before = System.currentTimeMillis(); + imp.refreshSplits(1); + long after = System.currentTimeMillis(); + + List options = optionsCaptor.getAllValues(); + Assert.assertEquals(options.size(), 13); + Assert.assertFalse(options.get(0).hasCustomCN()); + Assert.assertFalse(options.get(1).hasCustomCN()); + Assert.assertFalse(options.get(2).hasCustomCN()); + Assert.assertTrue(options.get(3).hasCustomCN()); + Assert.assertTrue(options.get(4).hasCustomCN()); + Assert.assertTrue(options.get(5).hasCustomCN()); + Assert.assertTrue(options.get(6).hasCustomCN()); + Assert.assertTrue(options.get(7).hasCustomCN()); + Assert.assertTrue(options.get(8).hasCustomCN()); + Assert.assertTrue(options.get(9).hasCustomCN()); + Assert.assertTrue(options.get(10).hasCustomCN()); + Assert.assertTrue(options.get(11).hasCustomCN()); + Assert.assertTrue(options.get(12).hasCustomCN()); + + Assert.assertEquals(calls.get(), 13); + long minDiffExpected = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256; + Assert.assertTrue((after - before) > minDiffExpected); + } + + @Test + public void testCDNBypassRequestLimitAndForSegmentsBackoff() throws NoSuchFieldException, IllegalAccessException { + + SplitCache cache = new InMemoryCacheImp(); + Synchronizer imp = new SynchronizerImp(_refreshableSplitFetcherTask, + _splitFetcher, + _segmentFetcher, + cache, + _segmentCache, + 50, + 3, + 1, + true, + Mockito.mock(SDKReadinessGates.class)); + + SegmentFetcher fetcher = Mockito.mock(SegmentFetcher.class); + when(_segmentFetcher.getFetcher("someSegment")).thenReturn(fetcher); + + ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class); + AtomicInteger calls = new AtomicInteger(); + Mockito.doAnswer(invocationOnMock -> { + calls.getAndIncrement(); + switch (calls.get()) { + case 14: Assert.assertTrue(false); // should never get here + } + return null; + }).when(fetcher).fetch(optionsCaptor.capture()); + + // Before executing, we'll update the backoff via reflection, to avoid waiting minutes for the test to run. + Field backoffBase = SynchronizerImp.class.getDeclaredField("ON_DEMAND_FETCH_BACKOFF_BASE_MS"); + backoffBase.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(backoffBase, backoffBase.getModifiers() & ~Modifier.FINAL); + backoffBase.set(imp, 1); // 1ms + + long before = System.currentTimeMillis(); + imp.refreshSegment("someSegment",1); + long after = System.currentTimeMillis(); + + List options = optionsCaptor.getAllValues(); + Assert.assertEquals(options.size(), 13); + Assert.assertFalse(options.get(0).hasCustomCN()); + Assert.assertFalse(options.get(1).hasCustomCN()); + Assert.assertFalse(options.get(2).hasCustomCN()); + Assert.assertTrue(options.get(3).hasCustomCN()); + Assert.assertTrue(options.get(4).hasCustomCN()); + Assert.assertTrue(options.get(5).hasCustomCN()); + Assert.assertTrue(options.get(6).hasCustomCN()); + Assert.assertTrue(options.get(7).hasCustomCN()); + Assert.assertTrue(options.get(8).hasCustomCN()); + Assert.assertTrue(options.get(9).hasCustomCN()); + Assert.assertTrue(options.get(10).hasCustomCN()); + Assert.assertTrue(options.get(11).hasCustomCN()); + Assert.assertTrue(options.get(12).hasCustomCN()); + + Assert.assertEquals(calls.get(), 13); + long minDiffExpected = 1 + 2 + 4 + 8 + 16 + 32 + 64 + 128 + 256; + Assert.assertTrue((after - before) > minDiffExpected); + } } diff --git a/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java b/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java index 6b0114566..64495e112 100644 --- a/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java +++ b/client/src/test/java/io/split/engine/experiments/AChangePerCallSplitChangeFetcher.java @@ -7,6 +7,7 @@ import io.split.client.dtos.SplitChange; import io.split.client.dtos.Status; import io.split.engine.ConditionsTestUtil; +import io.split.engine.common.FetchOptions; import io.split.grammar.Treatments; import java.util.concurrent.atomic.AtomicLong; @@ -31,7 +32,7 @@ public AChangePerCallSplitChangeFetcher(String segmentName) { @Override - public SplitChange fetch(long since, boolean addCacheHeader) { + public SplitChange fetch(long since, FetchOptions options) { long latestChangeNumber = since + 1; Condition condition = null; 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 cb9352994..9999da832 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitFetcherTest.java @@ -8,14 +8,22 @@ import io.split.client.dtos.*; import io.split.engine.ConditionsTestUtil; import io.split.engine.SDKReadinessGates; +import io.split.engine.common.FetchOptions; import io.split.engine.matchers.AllKeysMatcher; import io.split.engine.matchers.CombiningMatcher; import io.split.engine.segments.SegmentChangeFetcher; import io.split.engine.segments.SegmentSynchronizationTask; import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.grammar.Treatments; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; import org.mockito.Mockito; +import org.mockito.internal.matchers.Any; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -40,6 +48,7 @@ */ public class SplitFetcherTest { private static final Logger _log = LoggerFactory.getLogger(SplitFetcherTest.class); + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); @Test public void works_when_we_start_without_any_state() throws InterruptedException { @@ -56,9 +65,9 @@ private void works(long startingChangeNumber) throws InterruptedException { SDKReadinessGates gates = new SDKReadinessGates(); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1,10, gates, segmentCache); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1,10, gates, segmentCache, TELEMETRY_STORAGE); SplitCache cache = new InMemoryCacheImp(startingChangeNumber); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), gates, cache); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 3, TimeUnit.SECONDS); @@ -77,9 +86,8 @@ private void works(long startingChangeNumber) throws InterruptedException { ParsedSplit expected = ParsedSplit.createParsedSplitForTests("" + cache.getChangeNumber(), (int) cache.getChangeNumber(), false, Treatments.OFF, expectedListOfMatcherAndSplits, null, cache.getChangeNumber(), 1); ParsedSplit actual = cache.get("" + cache.getChangeNumber()); - + Thread.sleep(1000); assertThat(actual, is(equalTo(expected))); - assertThat(gates.areSplitsReady(0), is(equalTo(true))); } @Test @@ -122,17 +130,17 @@ public void when_parser_fails_we_remove_the_experiment() throws InterruptedExcep noReturn.till = 1L; SplitChangeFetcher splitChangeFetcher = mock(SplitChangeFetcher.class); - when(splitChangeFetcher.fetch(-1L, false)).thenReturn(validReturn); - when(splitChangeFetcher.fetch(0L, false)).thenReturn(invalidReturn); - when(splitChangeFetcher.fetch(1L, false)).thenReturn(noReturn); + when(splitChangeFetcher.fetch(Mockito.eq(-1L), Mockito.any())).thenReturn(validReturn); + when(splitChangeFetcher.fetch(Mockito.eq(0L), Mockito.any())).thenReturn(invalidReturn); + when(splitChangeFetcher.fetch(Mockito.eq(1L), Mockito.any())).thenReturn(noReturn); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, gates, segmentCache); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, gates, segmentCache, TELEMETRY_STORAGE); segmentSynchronizationTask.startPeriodicFetching(); SplitCache cache = new InMemoryCacheImp(-1); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), new SDKReadinessGates(), cache); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -148,19 +156,18 @@ public void if_there_is_a_problem_talking_to_split_change_count_down_latch_is_no SplitCache cache = new InMemoryCacheImp(-1); SplitChangeFetcher splitChangeFetcher = mock(SplitChangeFetcher.class); - when(splitChangeFetcher.fetch(-1L, false)).thenThrow(new RuntimeException()); + when(splitChangeFetcher.fetch(-1L, new FetchOptions.Builder().build())).thenThrow(new RuntimeException()); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, gates, segmentCache); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, gates, segmentCache, TELEMETRY_STORAGE); segmentSynchronizationTask.startPeriodicFetching(); - SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), gates, cache); + SplitFetcherImp fetcher = new SplitFetcherImp(splitChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); assertThat(cache.getChangeNumber(), is(equalTo(-1L))); - assertThat(gates.areSplitsReady(0), is(equalTo(false))); } private void executeWaitAndTerminate(Runnable runnable, long frequency, long waitInBetween, TimeUnit unit) throws InterruptedException { @@ -193,10 +200,10 @@ public void works_with_user_defined_segments() throws Exception { SegmentChangeFetcher segmentChangeFetcher = mock(SegmentChangeFetcher.class); SegmentChange segmentChange = getSegmentChange(0L, 0L, segmentName); - when(segmentChangeFetcher.fetch(anyString(), anyLong(), anyBoolean())).thenReturn(segmentChange); - SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, gates, segmentCache); + when(segmentChangeFetcher.fetch(anyString(), anyLong(), any())).thenReturn(segmentChange); + SegmentSynchronizationTask segmentSynchronizationTask = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1,10, gates, segmentCache, Mockito.mock(TelemetryStorage.class)); segmentSynchronizationTask.startPeriodicFetching(); - SplitFetcherImp fetcher = new SplitFetcherImp(experimentChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), gates, cache); + SplitFetcherImp fetcher = new SplitFetcherImp(experimentChangeFetcher, new SplitParser(segmentSynchronizationTask, segmentCache), cache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. executeWaitAndTerminate(fetcher, 1, 5, TimeUnit.SECONDS); @@ -209,11 +216,50 @@ public void works_with_user_defined_segments() throws Exception { assertThat("Asking for " + i + " " + cache.getAll(), cache.get("" + i), is(not(nullValue()))); assertThat(cache.get("" + i).killed(), is(true)); } + } + + @Test + public void testBypassCdnClearedAfterFirstHit() { + SplitChangeFetcher mockFetcher = Mockito.mock(SplitChangeFetcher.class); + SegmentSynchronizationTask segmentSynchronizationTaskMock = Mockito.mock(SegmentSynchronizationTask.class); + SegmentCache segmentCacheMock = Mockito.mock(SegmentCache.class); + SplitParser mockParser = new SplitParser(segmentSynchronizationTaskMock, segmentCacheMock); + SDKReadinessGates mockGates = Mockito.mock(SDKReadinessGates.class); + SplitCache mockCache = new InMemoryCacheImp(); + SplitFetcherImp fetcher = new SplitFetcherImp(mockFetcher, mockParser, mockCache, Mockito.mock(TelemetryRuntimeProducer.class)); + + + SplitChange response1 = new SplitChange(); + response1.splits = new ArrayList<>(); + response1.since = -1; + response1.till = 1; + + SplitChange response2 = new SplitChange(); + response2.splits = new ArrayList<>(); + response2.since = 1; + response2.till = 1; + + + ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class); + ArgumentCaptor cnCaptor = ArgumentCaptor.forClass(Long.class); + when(mockFetcher.fetch(cnCaptor.capture(), optionsCaptor.capture())).thenReturn(response1, response2); + + FetchOptions originalOptions = new FetchOptions.Builder().targetChangeNumber(123).build(); + fetcher.forceRefresh(originalOptions); + List capturedCNs = cnCaptor.getAllValues(); + List capturedOptions = optionsCaptor.getAllValues(); + + Assert.assertEquals(capturedOptions.size(), 2); + Assert.assertEquals(capturedCNs.size(), 2); + + Assert.assertEquals(capturedCNs.get(0), Long.valueOf(-1)); + Assert.assertEquals(capturedCNs.get(1), Long.valueOf(1)); + + Assert.assertEquals(capturedOptions.get(0).targetCN(), 123); + Assert.assertEquals(capturedOptions.get(1).targetCN(), -1); - assertThat(gates.areSplitsReady(0), is(equalTo(true))); - assertThat(gates.isSegmentRegistered(segmentName), is(equalTo(true))); - assertThat(gates.areSegmentsReady(100), is(equalTo(true))); - assertThat(gates.isSDKReady(0), is(equalTo(true))); + // Ensure that the original value hasn't been modified + Assert.assertEquals(originalOptions.targetCN(), 123); } private SegmentChange getSegmentChange(long since, long till, String segmentName){ diff --git a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java index 86beed261..83518da0e 100644 --- a/client/src/test/java/io/split/engine/experiments/SplitParserTest.java +++ b/client/src/test/java/io/split/engine/experiments/SplitParserTest.java @@ -24,6 +24,9 @@ import io.split.engine.segments.SegmentSynchronizationTask; import io.split.engine.segments.SegmentSynchronizationTaskImp; import io.split.grammar.Treatments; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Assert; import org.junit.Test; import org.mockito.Mockito; @@ -36,7 +39,6 @@ import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.nullValue; import static org.junit.Assert.assertThat; /** @@ -48,6 +50,8 @@ public class SplitParserTest { public static final String EMPLOYEES = "employees"; public static final String SALES_PEOPLE = "salespeople"; + public static final int CONDITIONS_UPPER_LIMIT = 50; + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); @Test public void works() { @@ -58,9 +62,9 @@ public void works() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -97,9 +101,9 @@ public void worksWithConfig() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -141,9 +145,9 @@ public void works_for_two_conditions() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -173,16 +177,16 @@ public void works_for_two_conditions() { } @Test - public void fails_for_long_conditions() { + public void success_for_long_conditions() { SDKReadinessGates gates = new SDKReadinessGates(); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); segmentCache.updateSegment(EMPLOYEES, Stream.of("adil", "pato", "trevor").collect(Collectors.toList()), new ArrayList<>()); segmentCache.updateSegment(SALES_PEOPLE, Stream.of("kunal").collect(Collectors.toList()), new ArrayList<>()); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -190,14 +194,14 @@ public void fails_for_long_conditions() { List conditions = Lists.newArrayList(); List p1 = Lists.newArrayList(ConditionsTestUtil.partition("on", 100)); - for (int i = 0 ; i < SplitParser.CONDITIONS_UPPER_LIMIT+1 ; i++) { + for (int i = 0 ; i < CONDITIONS_UPPER_LIMIT+1 ; i++) { Condition c = ConditionsTestUtil.and(employeesMatcher, p1); conditions.add(c); } Split split = makeSplit("first.name", 123, conditions, 1); - assertThat(parser.parse(split), is(nullValue())); + Assert.assertNotNull(parser.parse(split)); } @@ -210,9 +214,9 @@ public void works_with_attributes() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -256,9 +260,9 @@ public void less_than_or_equal_to() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -295,9 +299,9 @@ public void equal_to() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -334,9 +338,9 @@ public void equal_to_negative_number() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -373,9 +377,9 @@ public void between() { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); @@ -550,9 +554,9 @@ public void set_matcher_test(Condition c, io.split.engine.matchers.Matcher m) { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChangeEmployee = getSegmentChange(-1L, -1L, EMPLOYEES); SegmentChange segmentChangeSalesPeople = getSegmentChange(-1L, -1L, SALES_PEOPLE); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChangeEmployee).thenReturn(segmentChangeSalesPeople); - SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache); + SegmentSynchronizationTask segmentFetcher = new SegmentSynchronizationTaskImp(segmentChangeFetcher,1L, 1, gates, segmentCache, TELEMETRY_STORAGE); SplitParser parser = new SplitParser(segmentFetcher, segmentCache); 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 79786bdd6..0f0c799e0 100644 --- a/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java +++ b/client/src/test/java/io/split/engine/segments/SegmentFetcherImpTest.java @@ -1,11 +1,23 @@ package io.split.engine.segments; import com.google.common.collect.Sets; +import io.split.cache.InMemoryCacheImp; import io.split.cache.SegmentCache; import io.split.cache.SegmentCacheInMemoryImpl; +import io.split.cache.SplitCache; import io.split.client.dtos.SegmentChange; +import io.split.client.dtos.SplitChange; import io.split.engine.SDKReadinessGates; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.telemetry.storage.TelemetryStorage; +import io.split.engine.common.FetchOptions; +import io.split.engine.experiments.SplitChangeFetcher; +import io.split.engine.experiments.SplitFetcherImp; +import io.split.engine.experiments.SplitParser; +import org.junit.Assert; import org.junit.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,6 +33,7 @@ import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.when; /** * Tests for RefreshableSegmentFetcher. @@ -30,6 +43,7 @@ public class SegmentFetcherImpTest { private static final Logger _log = LoggerFactory.getLogger(SegmentFetcherImpTest.class); private static final String SEGMENT_NAME = "foo"; + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); @Test public void works_when_we_start_without_state() throws InterruptedException { @@ -46,14 +60,13 @@ public void works_when_we_start_with_state() throws InterruptedException { public void works_when_there_are_no_changes() throws InterruptedException { long startingChangeNumber = -1L; SDKReadinessGates gates = new SDKReadinessGates(); - gates.registerSegment(SEGMENT_NAME); SegmentCache segmentCache = new SegmentCacheInMemoryImpl(); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChange = getSegmentChange(-1L, 10L); - Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean())).thenReturn(segmentChange); + Mockito.when(segmentChangeFetcher.fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.any())).thenReturn(segmentChange); - SegmentFetcherImp fetcher = new SegmentFetcherImp(SEGMENT_NAME, segmentChangeFetcher, gates, segmentCache); + SegmentFetcherImp fetcher = new SegmentFetcherImp(SEGMENT_NAME, segmentChangeFetcher, gates, segmentCache, TELEMETRY_STORAGE); // execute the fetcher for a little bit. ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); @@ -76,14 +89,12 @@ public void works_when_there_are_no_changes() throws InterruptedException { assertNotNull(segmentCache.getChangeNumber(SEGMENT_NAME)); assertEquals(10L, segmentCache.getChangeNumber(SEGMENT_NAME)); - assertThat(gates.areSegmentsReady(10), is(true)); } private void works(long startingChangeNumber) throws InterruptedException { SDKReadinessGates gates = new SDKReadinessGates(); String segmentName = SEGMENT_NAME; - gates.registerSegment(segmentName); SegmentCache segmentCache = Mockito.mock(SegmentCache.class); Mockito.when(segmentCache.getChangeNumber(SEGMENT_NAME)).thenReturn(-1L).thenReturn(-1L) .thenReturn(-1L) @@ -92,9 +103,9 @@ private void works(long startingChangeNumber) throws InterruptedException { SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); SegmentChange segmentChange = getSegmentChange(-1L, -1L); - Mockito.when(segmentChangeFetcher.fetch(SEGMENT_NAME, -1L, false)).thenReturn(segmentChange); - Mockito.when(segmentChangeFetcher.fetch(SEGMENT_NAME, 0L, false)).thenReturn(segmentChange); - SegmentFetcher fetcher = new SegmentFetcherImp(segmentName, segmentChangeFetcher, gates, segmentCache); + Mockito.when(segmentChangeFetcher.fetch(Mockito.eq(SEGMENT_NAME),Mockito.eq( -1L), Mockito.any())).thenReturn(segmentChange); + Mockito.when(segmentChangeFetcher.fetch(Mockito.eq(SEGMENT_NAME),Mockito.eq( 0L), Mockito.any())).thenReturn(segmentChange); + SegmentFetcher fetcher = new SegmentFetcherImp(segmentName, segmentChangeFetcher, gates, segmentCache, Mockito.mock(TelemetryRuntimeProducer.class)); // execute the fetcher for a little bit. ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor(); @@ -112,8 +123,7 @@ private void works(long startingChangeNumber) throws InterruptedException { // reset the interrupt. Thread.currentThread().interrupt(); } - Mockito.verify(segmentChangeFetcher, Mockito.times(2)).fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyBoolean()); - assertThat(gates.areSegmentsReady(10), is(true)); + Mockito.verify(segmentChangeFetcher, Mockito.times(2)).fetch(Mockito.anyString(), Mockito.anyLong(), Mockito.anyObject()); } @@ -121,21 +131,66 @@ private void works(long startingChangeNumber) throws InterruptedException { @Test(expected = NullPointerException.class) public void does_not_work_if_segment_change_fetcher_is_null() { SegmentCache segmentCache = Mockito.mock(SegmentCache.class); - SegmentFetcher fetcher = new SegmentFetcherImp(SEGMENT_NAME, null, new SDKReadinessGates(), segmentCache); + SegmentFetcher fetcher = new SegmentFetcherImp(SEGMENT_NAME, null, new SDKReadinessGates(), segmentCache, TELEMETRY_STORAGE); } @Test(expected = NullPointerException.class) public void does_not_work_if_segment_name_is_null() { SegmentCache segmentCache = Mockito.mock(SegmentCache.class); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); - SegmentFetcher fetcher = new SegmentFetcherImp(null, segmentChangeFetcher, new SDKReadinessGates(), segmentCache); + SegmentFetcher fetcher = new SegmentFetcherImp(null, segmentChangeFetcher, new SDKReadinessGates(), segmentCache, TELEMETRY_STORAGE); } @Test(expected = NullPointerException.class) public void does_not_work_if_sdk_readiness_gates_are_null() { SegmentCache segmentCache = Mockito.mock(SegmentCache.class); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); - SegmentFetcher fetcher = new SegmentFetcherImp(SEGMENT_NAME, segmentChangeFetcher, null, segmentCache); + SegmentFetcher fetcher = new SegmentFetcherImp(SEGMENT_NAME, segmentChangeFetcher, null, segmentCache, TELEMETRY_STORAGE); + } + + @Test + public void testBypassCdnClearedAfterFirstHit() { + SegmentChangeFetcher mockFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentSynchronizationTask segmentSynchronizationTaskMock = Mockito.mock(SegmentSynchronizationTask.class); + SegmentCache segmentCacheMock = new SegmentCacheInMemoryImpl(); + SDKReadinessGates mockGates = Mockito.mock(SDKReadinessGates.class); + SegmentFetcher fetcher = new SegmentFetcherImp("someSegment", mockFetcher, mockGates, segmentCacheMock, Mockito.mock(TelemetryRuntimeProducer.class)); + + + SegmentChange response1 = new SegmentChange(); + response1.name = "someSegment"; + response1.added = new ArrayList<>(); + response1.removed = new ArrayList<>(); + response1.since = -1; + response1.till = 1; + + SegmentChange response2 = new SegmentChange(); + response2.name = "someSegment"; + response2.added = new ArrayList<>(); + response2.removed = new ArrayList<>(); + response2.since = 1; + response1.till = 1; + + ArgumentCaptor optionsCaptor = ArgumentCaptor.forClass(FetchOptions.class); + ArgumentCaptor cnCaptor = ArgumentCaptor.forClass(Long.class); + when(mockFetcher.fetch(Mockito.eq("someSegment"), cnCaptor.capture(), optionsCaptor.capture())).thenReturn(response1, response2); + + FetchOptions originalOptions = new FetchOptions.Builder().targetChangeNumber(123).build(); + fetcher.fetch(originalOptions); + List capturedCNs = cnCaptor.getAllValues(); + List capturedOptions = optionsCaptor.getAllValues(); + + Assert.assertEquals(capturedOptions.size(), 2); + Assert.assertEquals(capturedCNs.size(), 2); + + Assert.assertEquals(capturedCNs.get(0), Long.valueOf(-1)); + Assert.assertEquals(capturedCNs.get(1), Long.valueOf(1)); + + Assert.assertEquals(capturedOptions.get(0).targetCN(), 123); + Assert.assertEquals(capturedOptions.get(1).targetCN(), -1); + + // Ensure that the original value hasn't been modified + Assert.assertEquals(originalOptions.targetCN(), 123); } private SegmentChange getSegmentChange(long since, long till){ 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 b856c1d78..a4e38a129 100644 --- a/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java +++ b/client/src/test/java/io/split/engine/segments/SegmentSynchronizationTaskImpTest.java @@ -1,14 +1,21 @@ package io.split.engine.segments; +import com.google.common.collect.Maps; import io.split.engine.SDKReadinessGates; import io.split.cache.SegmentCache; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.Mockito; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; import java.util.List; +import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -24,6 +31,7 @@ */ public class SegmentSynchronizationTaskImpTest { private static final Logger _log = LoggerFactory.getLogger(SegmentSynchronizationTaskImpTest.class); + private static final TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); private AtomicReference fetcher1 = null; private AtomicReference fetcher2 = null; @@ -40,7 +48,7 @@ public void works() { SegmentCache segmentCache = Mockito.mock(SegmentCache.class); SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); - final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, gates, segmentCache); + final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, gates, segmentCache, TELEMETRY_STORAGE); // create two tasks that will separately call segment and make sure @@ -72,11 +80,58 @@ public void run() { Thread.currentThread().interrupt(); } - gates.splitsAreReady(); - assertThat(fetcher1.get(), is(notNullValue())); assertThat(fetcher1.get(), is(sameInstance(fetcher2.get()))); } + @Test + public void testFetchAllAsynchronousAndGetFalse() throws NoSuchFieldException, IllegalAccessException { + SDKReadinessGates gates = new SDKReadinessGates(); + SegmentCache segmentCache = Mockito.mock(SegmentCache.class); + ConcurrentMap _segmentFetchers = Maps.newConcurrentMap(); + + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentFetcherImp segmentFetcher = Mockito.mock(SegmentFetcherImp.class); + _segmentFetchers.put("SF", segmentFetcher); + final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, gates, segmentCache, TELEMETRY_STORAGE); + Mockito.doNothing().when(segmentFetcher).callLoopRun(Mockito.anyObject()); + Mockito.when(segmentFetcher.runWhitCacheHeader()).thenReturn(false); + Mockito.when(segmentFetcher.fetchAndUpdate(Mockito.anyObject())).thenReturn(false); + Mockito.doNothing().when(segmentFetcher).callLoopRun(Mockito.anyObject()); + + // Before executing, we'll update the map of segmentFecthers via reflection. + Field segmentFetchersForced = SegmentSynchronizationTaskImp.class.getDeclaredField("_segmentFetchers"); + segmentFetchersForced.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(segmentFetchersForced, segmentFetchersForced.getModifiers() & ~Modifier.FINAL); + + segmentFetchersForced.set(fetchers, _segmentFetchers); + boolean fetch = fetchers.fetchAllSynchronous(); + Assert.assertEquals(false, fetch); + } + @Test + public void testFetchAllAsynchronousAndGetTrue() throws NoSuchFieldException, IllegalAccessException { + SDKReadinessGates gates = new SDKReadinessGates(); + SegmentCache segmentCache = Mockito.mock(SegmentCache.class); + + ConcurrentMap _segmentFetchers = Maps.newConcurrentMap(); + SegmentChangeFetcher segmentChangeFetcher = Mockito.mock(SegmentChangeFetcher.class); + SegmentFetcherImp segmentFetcher = Mockito.mock(SegmentFetcherImp.class); + final SegmentSynchronizationTaskImp fetchers = new SegmentSynchronizationTaskImp(segmentChangeFetcher, 1L, 1, gates, segmentCache, TELEMETRY_STORAGE); + + // Before executing, we'll update the map of segmentFecthers via reflection. + Field segmentFetchersForced = SegmentSynchronizationTaskImp.class.getDeclaredField("_segmentFetchers"); + segmentFetchersForced.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(segmentFetchersForced, segmentFetchersForced.getModifiers() & ~Modifier.FINAL); + segmentFetchersForced.set(fetchers, _segmentFetchers); + Mockito.doNothing().when(segmentFetcher).callLoopRun(Mockito.anyObject()); + Mockito.when(segmentFetcher.runWhitCacheHeader()).thenReturn(true); + Mockito.when(segmentFetcher.fetchAndUpdate(Mockito.anyObject())).thenReturn(true); + boolean fetch = fetchers.fetchAllSynchronous(); + Assert.assertEquals(true, fetch); + } } diff --git a/client/src/test/java/io/split/engine/sse/AuthApiClientTest.java b/client/src/test/java/io/split/engine/sse/AuthApiClientTest.java index 2debfe68c..dab743602 100644 --- a/client/src/test/java/io/split/engine/sse/AuthApiClientTest.java +++ b/client/src/test/java/io/split/engine/sse/AuthApiClientTest.java @@ -2,21 +2,31 @@ import io.split.TestHelper; import io.split.engine.sse.dtos.AuthenticationResponse; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; import org.apache.commons.lang3.StringUtils; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.core5.http.HttpStatus; import org.junit.Assert; +import org.junit.Before; import org.junit.Test; +import org.mockito.Mockito; import java.io.IOException; import java.lang.reflect.InvocationTargetException; public class AuthApiClientTest { + private static TelemetryStorage TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + + @Before + public void setUp() { + TELEMETRY_STORAGE = Mockito.mock(InMemoryTelemetryStorage.class); + } @Test public void authenticateWithPushEnabledShouldReturnSuccess() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("streaming-auth-push-enabled.json", HttpStatus.SC_OK); - AuthApiClient authApiClient = new AuthApiClientImp( "www.split-test.io", httpClientMock); + AuthApiClient authApiClient = new AuthApiClientImp( "www.split-test.io", httpClientMock, TELEMETRY_STORAGE); AuthenticationResponse result = authApiClient.Authenticate(); Assert.assertTrue(result.isPushEnabled()); @@ -24,6 +34,9 @@ public void authenticateWithPushEnabledShouldReturnSuccess() throws IOException, Assert.assertFalse(result.isRetry()); Assert.assertFalse(StringUtils.isEmpty(result.getToken())); Assert.assertTrue(result.getExpiration() > 0); + Mockito.verify(TELEMETRY_STORAGE, Mockito.times(1)).recordTokenRefreshes(); + Mockito.verify(TELEMETRY_STORAGE, Mockito.times(1)).recordSyncLatency(Mockito.anyObject(), Mockito.anyLong()); + Mockito.verify(TELEMETRY_STORAGE, Mockito.times(1)).recordSuccessfulSync(Mockito.anyObject(), Mockito.anyLong()); } @@ -31,7 +44,7 @@ public void authenticateWithPushEnabledShouldReturnSuccess() throws IOException, public void authenticateWithPushEnabledWithWrongTokenShouldReturnError() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("streaming-auth-push-enabled-wrong-token.json", HttpStatus.SC_OK); - AuthApiClient authApiClient = new AuthApiClientImp( "www.split-test.io", httpClientMock); + AuthApiClient authApiClient = new AuthApiClientImp( "www.split-test.io", httpClientMock, TELEMETRY_STORAGE); AuthenticationResponse result = authApiClient.Authenticate(); Assert.assertFalse(result.isPushEnabled()); @@ -45,7 +58,7 @@ public void authenticateWithPushEnabledWithWrongTokenShouldReturnError() throws public void authenticateWithPushDisabledShouldReturnSuccess() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("streaming-auth-push-disabled.json", HttpStatus.SC_OK); - AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock); + AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock, TELEMETRY_STORAGE); AuthenticationResponse result = authApiClient.Authenticate(); Assert.assertFalse(result.isPushEnabled()); @@ -58,7 +71,7 @@ public void authenticateWithPushDisabledShouldReturnSuccess() throws IOException public void authenticateServerErrorShouldReturnErrorWithRetry() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("", HttpStatus.SC_INTERNAL_SERVER_ERROR); - AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock); + AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock, TELEMETRY_STORAGE); AuthenticationResponse result = authApiClient.Authenticate(); Assert.assertFalse(result.isPushEnabled()); @@ -71,7 +84,7 @@ public void authenticateServerErrorShouldReturnErrorWithRetry() throws IOExcepti public void authenticateServerBadRequestShouldReturnErrorWithoutRetry() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("", HttpStatus.SC_BAD_REQUEST); - AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock); + AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock, TELEMETRY_STORAGE); AuthenticationResponse result = authApiClient.Authenticate(); Assert.assertFalse(result.isPushEnabled()); @@ -84,12 +97,13 @@ public void authenticateServerBadRequestShouldReturnErrorWithoutRetry() throws I public void authenticateServerUnauthorizedShouldReturnErrorWithoutRetry() throws IOException, IllegalAccessException, NoSuchMethodException, InvocationTargetException { CloseableHttpClient httpClientMock = TestHelper.mockHttpClient("", HttpStatus.SC_UNAUTHORIZED); - AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock); + AuthApiClient authApiClient = new AuthApiClientImp("www.split-test.io", httpClientMock, TELEMETRY_STORAGE); AuthenticationResponse result = authApiClient.Authenticate(); Assert.assertFalse(result.isPushEnabled()); Assert.assertTrue(StringUtils.isEmpty(result.getChannels())); Assert.assertTrue(StringUtils.isEmpty(result.getToken())); Assert.assertFalse(result.isRetry()); + Mockito.verify(TELEMETRY_STORAGE, Mockito.times(1)).recordAuthRejections(); } } 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 704e8dbba..74acf65e3 100644 --- a/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java +++ b/client/src/test/java/io/split/engine/sse/EventSourceClientTest.java @@ -4,6 +4,8 @@ import io.split.engine.sse.client.SSEClient; import io.split.engine.sse.dtos.ErrorNotification; import io.split.engine.sse.dtos.SplitChangeNotification; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; @@ -37,9 +39,10 @@ public void setUp() { public void startShouldConnect() throws IOException { SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); sseServer.start(); - EventSourceClient eventSourceClient = new EventSourceClientImp("http://localhost:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient()); + EventSourceClient eventSourceClient = new EventSourceClientImp("http://localhost:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient(), telemetryRuntimeProducer); boolean result = eventSourceClient.start("channel-test","token-test"); @@ -52,8 +55,9 @@ public void startShouldConnect() throws IOException { public void startShouldNotConnect() throws IOException { SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); sseServer.start(); - EventSourceClient eventSourceClient = new EventSourceClientImp("http://fake:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient()); + EventSourceClient eventSourceClient = new EventSourceClientImp("http://fake:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient(), telemetryRuntimeProducer); boolean result = eventSourceClient.start("channel-test","token-test"); @@ -68,8 +72,9 @@ public void startShouldNotConnect() throws IOException { public void startAndReceiveNotification() throws IOException { SSEMockServer.SseEventQueue eventQueue = new SSEMockServer.SseEventQueue(); SSEMockServer sseServer = buildSSEMockServer(eventQueue); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); sseServer.start(); - EventSourceClient eventSourceClient = new EventSourceClientImp("http://localhost:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient()); + EventSourceClient eventSourceClient = new EventSourceClientImp("http://localhost:" + sseServer.getPort(), _notificationParser, _notificationProcessor, _pushStatusTracker, buildHttpClient(), telemetryRuntimeProducer); boolean result = eventSourceClient.start("channel-test","token-test"); @@ -106,6 +111,8 @@ public void startAndReceiveNotification() throws IOException { Awaitility.await() .atMost(50L, TimeUnit.SECONDS) .untilAsserted(() -> Mockito.verify(_pushStatusTracker, Mockito.times(1)).handleIncomingAblyError(Mockito.any(ErrorNotification.class))); + + Mockito.verify(_pushStatusTracker, Mockito.times(1)).handleSseStatus(SSEClient.StatusMessage.FIRST_EVENT); } private SSEMockServer buildSSEMockServer(SSEMockServer.SseEventQueue eventQueue) { 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 82d8fc554..4e3f2dbf8 100644 --- a/client/src/test/java/io/split/engine/sse/PushStatusTrackerTest.java +++ b/client/src/test/java/io/split/engine/sse/PushStatusTrackerTest.java @@ -1,21 +1,31 @@ package io.split.engine.sse; import io.split.engine.common.PushManager; +import io.split.engine.sse.client.SSEClient; import io.split.engine.sse.dtos.*; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryStorage; +import org.junit.Assert; import org.junit.Test; +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"; + private static final String CONTROL_SEC = "control_sec"; @Test public void HandleControlEventStreamingPausedShouldNotifyEvent() { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); ControlNotification controlNotification = buildControlNotification(ControlType.STREAMING_PAUSED); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingControlEvent(controlNotification); assertThat(messages.size(), is(equalTo(1))); @@ -24,8 +34,9 @@ public void HandleControlEventStreamingPausedShouldNotifyEvent() { @Test public void HandleControlEventStreamingResumedShouldNotifyEvent() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingControlEvent(buildControlNotification(ControlType.STREAMING_PAUSED)); pushStatusTracker.handleIncomingControlEvent(buildControlNotification(ControlType.STREAMING_RESUMED)); @@ -36,25 +47,28 @@ public void HandleControlEventStreamingResumedShouldNotifyEvent() throws Interru @Test public void HandleControlEventStreamingResumedShouldNotNotifyEvent() { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); - OccupancyNotification occupancyNotification = buildOccupancyNotification(0, null); + OccupancyNotification occupancyNotification = buildOccupancyNotification(0, CONTROL_PRI); ControlNotification controlNotification = buildControlNotification(ControlType.STREAMING_RESUMED); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingOccupancyEvent(occupancyNotification); pushStatusTracker.handleIncomingControlEvent(controlNotification); pushStatusTracker.handleIncomingControlEvent(controlNotification); assertThat(messages.size(), is(equalTo(1))); assertThat(messages.peek(), is(equalTo(PushManager.Status.STREAMING_DOWN))); + Assert.assertEquals(1, telemetryStorage.popStreamingEvents().size()); } @Test public void HandleControlEventStreamingDisabledShouldNotifyShutdownEvent() { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); ControlNotification controlNotification = buildControlNotification(ControlType.STREAMING_DISABLED); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingControlEvent(controlNotification); pushStatusTracker.handleIncomingControlEvent(controlNotification); @@ -64,18 +78,21 @@ public void HandleControlEventStreamingDisabledShouldNotifyShutdownEvent() { @Test public void HandleOccupancyEventWithPublishersFirstTimeShouldNotNotifyEvent() { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); - OccupancyNotification occupancyNotification = buildOccupancyNotification(2, null); + OccupancyNotification occupancyNotification = buildOccupancyNotification(2, CONTROL_SEC); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingOccupancyEvent(occupancyNotification); assertThat(messages.size(), is(equalTo(0))); + Assert.assertEquals(1, telemetryStorage.popStreamingEvents().size()); } @Test public void HandleOccupancyEventWithPublishersAndWithStreamingDisabledShouldNotifyEvent() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(0, null)); pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(2, null)); @@ -89,8 +106,9 @@ public void HandleOccupancyEventWithPublishersAndWithStreamingDisabledShouldNoti @Test public void HandleOccupancyEventWithDifferentChannelsPublishersShouldNotifyEvent() throws InterruptedException { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); - PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(0, "control_pri")); pushStatusTracker.handleIncomingOccupancyEvent(buildOccupancyNotification(2, "control_sec")); @@ -102,6 +120,28 @@ public void HandleOccupancyEventWithDifferentChannelsPublishersShouldNotifyEvent assertThat(m2, is(equalTo(PushManager.Status.STREAMING_READY))); } + @Test + public void handleSSESTatusRecordTelemetryStreamingEvent() { + TelemetryStorage telemetryStorage = Mockito.mock(InMemoryTelemetryStorage.class); + LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); + pushStatusTracker.handleSseStatus(SSEClient.StatusMessage.CONNECTED); + pushStatusTracker.handleSseStatus(SSEClient.StatusMessage.FIRST_EVENT); + + Mockito.verify(telemetryStorage, Mockito.times(1)).recordStreamingEvents(Mockito.any()); + } + + @Test + public void handleAblyErrorRecordTelemetryStreamingEvent() { + TelemetryStorage telemetryStorage = Mockito.mock(InMemoryTelemetryStorage.class); + LinkedBlockingQueue messages = new LinkedBlockingQueue<>(); + PushStatusTracker pushStatusTracker = new PushStatusTrackerImp(messages, telemetryStorage); + pushStatusTracker.handleIncomingAblyError(new ErrorNotification("Error", "Ably error", 401)); + + Mockito.verify(telemetryStorage, Mockito.times(1)).recordStreamingEvents(Mockito.any()); + + } + private ControlNotification buildControlNotification(ControlType controlType) { return new ControlNotification(buildGenericData(controlType, IncomingNotification.Type.CONTROL,null, null)); } diff --git a/client/src/test/java/io/split/engine/sse/SSEClientTest.java b/client/src/test/java/io/split/engine/sse/SSEClientTest.java index 37cec7224..aa9e8ba97 100644 --- a/client/src/test/java/io/split/engine/sse/SSEClientTest.java +++ b/client/src/test/java/io/split/engine/sse/SSEClientTest.java @@ -1,6 +1,8 @@ package io.split.engine.sse; import io.split.engine.sse.client.SSEClient; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; import org.apache.hc.client5.http.config.RequestConfig; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; @@ -9,6 +11,7 @@ import org.apache.hc.core5.util.Timeout; import org.junit.Ignore; import org.junit.Test; +import org.mockito.Mockito; import java.net.URI; import java.net.URISyntaxException; @@ -23,6 +26,7 @@ public void basicUsageTest() throws URISyntaxException, InterruptedException { .addParameter("v", "1,1") .addParameter("channels", "[?occupancy=metrics.publishers]control_pri") .build(); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(InMemoryTelemetryStorage.class); RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(Timeout.ofMilliseconds(70000)) @@ -34,7 +38,7 @@ public void basicUsageTest() throws URISyntaxException, InterruptedException { CloseableHttpClient httpClient = httpClientbuilder.build(); SSEClient sse = new SSEClient(e -> { System.out.println(e); return null; }, - s -> { System.out.println(s); return null; }, httpClient); + s -> { System.out.println(s); return null; }, httpClient, telemetryRuntimeProducer); sse.open(uri); Thread.sleep(5000); sse.close(); diff --git a/client/src/test/java/io/split/service/HttpPostImpTest.java b/client/src/test/java/io/split/service/HttpPostImpTest.java new file mode 100644 index 000000000..60e71299c --- /dev/null +++ b/client/src/test/java/io/split/service/HttpPostImpTest.java @@ -0,0 +1,46 @@ +package io.split.service; + +import io.split.TestHelper; +import io.split.telemetry.domain.enums.HTTPLatenciesEnum; +import io.split.telemetry.domain.enums.LastSynchronizationRecordsEnum; +import io.split.telemetry.domain.enums.ResourceEnum; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.telemetry.storage.TelemetryStorage; +import junit.framework.TestCase; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.net.URI; + +public class HttpPostImpTest{ + + private static final String URL = "www.split.io"; + + @Test + public void testPostWith200() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException { + CloseableHttpClient client =TestHelper.mockHttpClient(URL, HttpStatus.SC_OK); + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); + HttpPostImp httpPostImp = new HttpPostImp(client, telemetryStorage); + httpPostImp.post(URI.create(URL), new Object(), "Metrics", HTTPLatenciesEnum.TELEMETRY, LastSynchronizationRecordsEnum.TELEMETRY, ResourceEnum.TELEMETRY_SYNC); + Mockito.verify(client, Mockito.times(1)).execute(Mockito.any()); + Assert.assertNotEquals(0, telemetryStorage.getLastSynchronization().get_telemetry()); + Assert.assertEquals(1, telemetryStorage.popHTTPLatencies().get_telemetry().stream().mapToInt(Long::intValue).sum()); + } + + @Test + public void testPostWith400() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException { + CloseableHttpClient client =TestHelper.mockHttpClient(URL, HttpStatus.SC_CLIENT_ERROR); + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); + HttpPostImp httpPostImp = new HttpPostImp(client, telemetryStorage); + httpPostImp.post(URI.create(URL), new Object(), "Metrics", HTTPLatenciesEnum.TELEMETRY, LastSynchronizationRecordsEnum.TELEMETRY, ResourceEnum.TELEMETRY_SYNC); + Mockito.verify(client, Mockito.times(1)).execute(Mockito.any()); + + Assert.assertEquals(1, telemetryStorage.popHTTPErrors().get_telemetry().get(Long.valueOf(HttpStatus.SC_CLIENT_ERROR)).intValue()); + } +} \ 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 new file mode 100644 index 000000000..e00544149 --- /dev/null +++ b/client/src/test/java/io/split/telemetry/storage/InMemoryTelemetryStorageTest.java @@ -0,0 +1,218 @@ +package io.split.telemetry.storage; + +import io.split.telemetry.domain.*; +import io.split.telemetry.domain.enums.*; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +public class InMemoryTelemetryStorageTest{ + + @Test + public void testInMemoryTelemetryStorage() throws Exception { + InMemoryTelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); + + //MethodLatencies + telemetryStorage.recordLatency(MethodEnum.TREATMENT, 1500l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENT, 2000l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENTS, 3000l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENTS, 500l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENT_WITH_CONFIG, 800l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENTS_WITH_CONFIG, 1000l * 1000); + + MethodLatencies latencies = telemetryStorage.popLatencies(); + Assert.assertEquals(2, latencies.get_treatment().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(2, latencies.get_treatments().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, latencies.get_treatmentsWithConfig().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, latencies.get_treatmentWithConfig().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, latencies.get_track().stream().mapToInt(Long::intValue).sum()); + + //Check empty has worked + latencies = telemetryStorage.popLatencies(); + Assert.assertEquals(0, latencies.get_treatment().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, latencies.get_treatments().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, latencies.get_treatmentsWithConfig().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, latencies.get_treatmentWithConfig().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, latencies.get_track().stream().mapToInt(Long::intValue).sum()); + + //HttpLatencies + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.TELEMETRY, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.TELEMETRY, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.EVENTS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.EVENTS, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SEGMENTS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SPLITS, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SPLITS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SPLITS, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.IMPRESSIONS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.IMPRESSIONS_COUNT, 2000l * 1000); + + HTTPLatencies httpLatencies = telemetryStorage.popHTTPLatencies(); + + Assert.assertEquals(3, httpLatencies.get_splits().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(2, httpLatencies.get_telemetry().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(2, httpLatencies.get_events().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, httpLatencies.get_segments().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, httpLatencies.get_impressions().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, httpLatencies.get_impressionsCount().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, httpLatencies.get_token().stream().mapToInt(Long::intValue).sum()); + + httpLatencies = telemetryStorage.popHTTPLatencies(); + Assert.assertEquals(0, httpLatencies.get_splits().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, httpLatencies.get_telemetry().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, httpLatencies.get_events().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, httpLatencies.get_segments().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, httpLatencies.get_impressions().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, httpLatencies.get_impressionsCount().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, httpLatencies.get_token().stream().mapToInt(Long::intValue).sum()); + + + //Exceptions + telemetryStorage.recordException(MethodEnum.TREATMENT); + telemetryStorage.recordException(MethodEnum.TREATMENTS); + telemetryStorage.recordException(MethodEnum.TREATMENT); + telemetryStorage.recordException(MethodEnum.TREATMENTS); + telemetryStorage.recordException(MethodEnum.TREATMENT_WITH_CONFIG); + telemetryStorage.recordException(MethodEnum.TREATMENTS_WITH_CONFIG); + + MethodExceptions methodExceptions = telemetryStorage.popExceptions(); + Assert.assertEquals(2, methodExceptions.get_treatment()); + Assert.assertEquals(2, methodExceptions.get_treatments()); + Assert.assertEquals(1, methodExceptions.get_treatmentsWithConfig()); + Assert.assertEquals(1, methodExceptions.get_treatmentWithConfig()); + Assert.assertEquals(0, methodExceptions.get_track()); + + //Check empty has worked + methodExceptions = telemetryStorage.popExceptions(); + Assert.assertEquals(0, methodExceptions.get_treatment()); + Assert.assertEquals(0, methodExceptions.get_treatments()); + Assert.assertEquals(0, methodExceptions.get_treatmentsWithConfig()); + Assert.assertEquals(0, methodExceptions.get_treatmentWithConfig()); + Assert.assertEquals(0, methodExceptions.get_track()); + + //AuthRejections + telemetryStorage.recordAuthRejections(); + long authRejections = telemetryStorage.popAuthRejections(); + Assert.assertEquals(1, authRejections); + + //Check amount has been reseted + authRejections = telemetryStorage.popAuthRejections(); + Assert.assertEquals(0, authRejections); + + //AuthRejections + telemetryStorage.recordTokenRefreshes(); + telemetryStorage.recordTokenRefreshes(); + long tokenRefreshes = telemetryStorage.popTokenRefreshes(); + Assert.assertEquals(2, tokenRefreshes); + + //Check amount has been reseted + tokenRefreshes = telemetryStorage.popTokenRefreshes(); + Assert.assertEquals(0, tokenRefreshes); + + //Non Ready usages + telemetryStorage.recordNonReadyUsage(); + telemetryStorage.recordNonReadyUsage(); + long nonReadyUsages = telemetryStorage.getNonReadyUsages(); + Assert.assertEquals(2, nonReadyUsages); + + //BUR Timeouts + telemetryStorage.recordBURTimeout(); + long burTimeouts = telemetryStorage.getBURTimeouts(); + Assert.assertEquals(1, burTimeouts); + + //ImpressionStats + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, 3); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, 1); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 4); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 6); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 2); + + long impressionsDeduped = telemetryStorage.getImpressionsStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED); + long impressionsDropped = telemetryStorage.getImpressionsStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED); + long impressionsQueued = telemetryStorage.getImpressionsStats(ImpressionsDataTypeEnum.IMPRESSIONS_QUEUED); + + Assert.assertEquals(4, impressionsDeduped); + Assert.assertEquals(12, impressionsDropped); + Assert.assertEquals(0, impressionsQueued); + + //Event Stats + telemetryStorage.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 3); + telemetryStorage.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 7); + telemetryStorage.recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 3); + + long eventsDropped = telemetryStorage.getEventStats(EventsDataRecordsEnum.EVENTS_DROPPED); + long eventsQueued = telemetryStorage.getEventStats(EventsDataRecordsEnum.EVENTS_QUEUED); + + Assert.assertEquals(10, eventsDropped); + Assert.assertEquals(3, eventsQueued); + + //Successfuly sync + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.EVENTS, 1500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.EVENTS, 800); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS, 2500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS, 10500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS_COUNT, 1500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.SEGMENTS, 1580); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.TELEMETRY, 265); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.TOKEN, 129); + + LastSynchronization lastSynchronization = telemetryStorage.getLastSynchronization(); + Assert.assertEquals(800, lastSynchronization.get_events()); + Assert.assertEquals(129, lastSynchronization.get_token()); + Assert.assertEquals(1580, lastSynchronization.get_segments()); + Assert.assertEquals(0, lastSynchronization.get_splits()); + Assert.assertEquals(10500, lastSynchronization.get_impressions()); + Assert.assertEquals(1500, lastSynchronization.get_impressionsCount()); + Assert.assertEquals(265, lastSynchronization.get_telemetry()); + + //Session length + telemetryStorage.recordSessionLength(91218); + long sessionLength = telemetryStorage.getSessionLength(); + Assert.assertEquals(91218, sessionLength); + + //Sync Error + telemetryStorage.recordSyncError(ResourceEnum.TELEMETRY_SYNC, 400); + telemetryStorage.recordSyncError(ResourceEnum.TELEMETRY_SYNC, 400); + telemetryStorage.recordSyncError(ResourceEnum.SEGMENT_SYNC, 501); + telemetryStorage.recordSyncError(ResourceEnum.IMPRESSION_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.IMPRESSION_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.EVENT_SYNC, 503); + telemetryStorage.recordSyncError(ResourceEnum.SPLIT_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.IMPRESSION_COUNT_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.TOKEN_SYNC, 403); + + HTTPErrors httpErrors = telemetryStorage.popHTTPErrors(); + Assert.assertEquals(2, httpErrors.get_telemetry().get(400l).intValue()); + Assert.assertEquals(1, httpErrors.get_segments().get(501l).intValue()); + Assert.assertEquals(2, httpErrors.get_impressions().get(403l).intValue()); + Assert.assertEquals(1, httpErrors.get_impressionsCount().get(403l).intValue()); + Assert.assertEquals(1, httpErrors.get_events().get(503l).intValue()); + Assert.assertEquals(1, httpErrors.get_splits().get(403l).intValue()); + Assert.assertEquals(1, httpErrors.get_token().get(403l).intValue()); + + //Streaming events + StreamingEvent streamingEvent = new StreamingEvent(1, 290, 91218); + telemetryStorage.recordStreamingEvents(streamingEvent); + + List streamingEvents = telemetryStorage.popStreamingEvents(); + Assert.assertEquals(290, streamingEvents.get(0).get_data()); + Assert.assertEquals(1, streamingEvents.get(0).get_type()); + Assert.assertEquals(91218, streamingEvents.get(0).getTimestamp()); + + //Check list has been cleared + streamingEvents = telemetryStorage.popStreamingEvents(); + Assert.assertEquals(0, streamingEvents.size()); + + //Tags + telemetryStorage.addTag("TAG_1"); + telemetryStorage.addTag("TAG_2"); + List tags = telemetryStorage.popTags(); + Assert.assertEquals(2, tags.size()); + + //Check tags have been cleared + tags = telemetryStorage.popTags(); + Assert.assertEquals(0, tags.size()); + + } +} diff --git a/client/src/test/java/io/split/telemetry/synchronizer/TelemetrySubmitterTest.java b/client/src/test/java/io/split/telemetry/synchronizer/TelemetrySubmitterTest.java new file mode 100644 index 000000000..d4bb42d06 --- /dev/null +++ b/client/src/test/java/io/split/telemetry/synchronizer/TelemetrySubmitterTest.java @@ -0,0 +1,224 @@ +package io.split.telemetry.synchronizer; + +import io.split.TestHelper; +import io.split.cache.SegmentCache; +import io.split.cache.SegmentCacheInMemoryImpl; +import io.split.cache.SplitCache; +import io.split.client.ApiKeyCounter; +import io.split.client.SplitClientConfig; +import io.split.telemetry.domain.Config; +import io.split.telemetry.domain.Stats; +import io.split.telemetry.domain.StreamingEvent; +import io.split.telemetry.domain.enums.*; +import io.split.telemetry.storage.InMemoryTelemetryStorage; +import io.split.telemetry.storage.TelemetryRuntimeProducer; +import io.split.telemetry.storage.TelemetryStorage; +import io.split.telemetry.storage.TelemetryStorageConsumer; +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.core5.http.HttpStatus; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class TelemetrySubmitterTest { + private static final String FIRST_KEY = "KEY_1"; + private static final String SECOND_KEY = "KEY_2"; + public static final String TELEMETRY_ENDPOINT = "https://telemetry.split.io/api/v1"; + + @Test + public void testSynchronizeConfig() throws URISyntaxException, NoSuchMethodException, IOException, IllegalAccessException, InvocationTargetException { + CloseableHttpClient httpClient = TestHelper.mockHttpClient(TELEMETRY_ENDPOINT, HttpStatus.SC_OK); + TelemetrySynchronizer telemetrySynchronizer = getTelemetrySynchronizer(httpClient); + SplitClientConfig splitClientConfig = SplitClientConfig.builder().build(); + + telemetrySynchronizer.synchronizeConfig(splitClientConfig, 100l, new HashMap(), new ArrayList()); + Mockito.verify(httpClient, Mockito.times(1)).execute(Mockito.any()); + } + + + @Test + public void testSynchronizeStats() throws Exception { + CloseableHttpClient httpClient = TestHelper.mockHttpClient(TELEMETRY_ENDPOINT, HttpStatus.SC_OK); + TelemetrySynchronizer telemetrySynchronizer = getTelemetrySynchronizer(httpClient); + + telemetrySynchronizer.synchronizeStats(); + Mockito.verify(httpClient, Mockito.times(1)).execute(Mockito.any()); + } + + @Test + public void testConfig() throws InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException, URISyntaxException, NoSuchFieldException, ClassNotFoundException { + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(FIRST_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(SECOND_KEY); + ApiKeyCounter.getApiKeyCounterInstance().add(SECOND_KEY); + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); + CloseableHttpClient httpClient = TestHelper.mockHttpClient(TELEMETRY_ENDPOINT, HttpStatus.SC_OK); + TelemetrySubmitter telemetrySynchronizer = getTelemetrySynchronizer(httpClient); + SplitClientConfig splitClientConfig = SplitClientConfig.builder().build(); + populateConfig(telemetryStorage); + Field teleTelemetryStorageConsumer = TelemetrySubmitter.class.getDeclaredField("_teleTelemetryStorageConsumer"); + teleTelemetryStorageConsumer.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(teleTelemetryStorageConsumer, teleTelemetryStorageConsumer.getModifiers() & ~Modifier.FINAL); + teleTelemetryStorageConsumer.set(telemetrySynchronizer, telemetryStorage); + Config config = telemetrySynchronizer.generateConfig(splitClientConfig, 100l, ApiKeyCounter.getApiKeyCounterInstance().getFactoryInstances(), new ArrayList<>()); + Assert.assertEquals(3, config.get_redundantFactories()); + Assert.assertEquals(2, config.get_burTimeouts()); + Assert.assertEquals(3, config.get_nonReadyUsages()); + } + + @Test + public void testStats() throws Exception { + TelemetryStorage telemetryStorage = new InMemoryTelemetryStorage(); + CloseableHttpClient httpClient = TestHelper.mockHttpClient(TELEMETRY_ENDPOINT, HttpStatus.SC_OK); + TelemetrySubmitter telemetrySynchronizer = getTelemetrySynchronizer(httpClient); + populateStats(telemetryStorage); + Field teleTelemetryStorageConsumer = TelemetrySubmitter.class.getDeclaredField("_teleTelemetryStorageConsumer"); + teleTelemetryStorageConsumer.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(teleTelemetryStorageConsumer, teleTelemetryStorageConsumer.getModifiers() & ~Modifier.FINAL); + + teleTelemetryStorageConsumer.set(telemetrySynchronizer, telemetryStorage); + Stats stats = telemetrySynchronizer.generateStats(); + Assert.assertEquals(2, stats.get_methodLatencies().get_treatment().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(2, stats.get_methodLatencies().get_treatments().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, stats.get_methodLatencies().get_treatmentsWithConfig().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, stats.get_methodLatencies().get_treatmentWithConfig().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, stats.get_methodLatencies().get_track().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(3, stats.get_httpLatencies().get_splits().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(2, stats.get_httpLatencies().get_telemetry().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(2, stats.get_httpLatencies().get_events().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, stats.get_httpLatencies().get_segments().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, stats.get_httpLatencies().get_impressions().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(1, stats.get_httpLatencies().get_impressionsCount().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(0, stats.get_httpLatencies().get_token().stream().mapToInt(Long::intValue).sum()); + Assert.assertEquals(2, stats.get_methodExceptions().get_treatment()); + Assert.assertEquals(2, stats.get_methodExceptions().get_treatments()); + Assert.assertEquals(1, stats.get_methodExceptions().get_treatmentsWithConfig()); + Assert.assertEquals(1, stats.get_methodExceptions().get_treatmentWithConfig()); + Assert.assertEquals(0, stats.get_methodExceptions().get_track()); + Assert.assertEquals(1, stats.get_authRejections()); + Assert.assertEquals(2, stats.get_tokenRefreshes()); + Assert.assertEquals(4, stats.get_impressionsDeduped()); + Assert.assertEquals(12, stats.get_impressionsDropped()); + Assert.assertEquals(0, stats.get_impressionsQueued()); + Assert.assertEquals(10, stats.get_eventsDropped()); + Assert.assertEquals(3, stats.get_eventsQueued()); + Assert.assertEquals(800, stats.get_lastSynchronization().get_events()); + Assert.assertEquals(129, stats.get_lastSynchronization().get_token()); + Assert.assertEquals(1580, stats.get_lastSynchronization().get_segments()); + Assert.assertEquals(0, stats.get_lastSynchronization().get_splits()); + Assert.assertEquals(10500, stats.get_lastSynchronization().get_impressions()); + Assert.assertEquals(1500, stats.get_lastSynchronization().get_impressionsCount()); + Assert.assertEquals(265, stats.get_lastSynchronization().get_telemetry()); + Assert.assertEquals(91218, stats.get_sessionLengthMs()); + Assert.assertEquals(2, stats.get_httpErrors().get_telemetry().get(400l).intValue()); + Assert.assertEquals(1, stats.get_httpErrors().get_segments().get(501l).intValue()); + Assert.assertEquals(2, stats.get_httpErrors().get_impressions().get(403l).intValue()); + Assert.assertEquals(1, stats.get_httpErrors().get_impressionsCount().get(403l).intValue()); + Assert.assertEquals(1, stats.get_httpErrors().get_events().get(503l).intValue()); + Assert.assertEquals(1, stats.get_httpErrors().get_splits().get(403l).intValue()); + Assert.assertEquals(1, stats.get_httpErrors().get_token().get(403l).intValue()); + List streamingEvents = stats.get_streamingEvents(); + Assert.assertEquals(290, streamingEvents.get(0).get_data()); + Assert.assertEquals(1, streamingEvents.get(0).get_type()); + Assert.assertEquals(91218, streamingEvents.get(0).getTimestamp()); + } + + private TelemetrySubmitter getTelemetrySynchronizer(CloseableHttpClient httpClient) throws URISyntaxException, InvocationTargetException, NoSuchMethodException, IllegalAccessException, IOException { + TelemetryStorageConsumer consumer = Mockito.mock(InMemoryTelemetryStorage.class); + TelemetryRuntimeProducer telemetryRuntimeProducer = Mockito.mock(TelemetryRuntimeProducer.class); + SplitCache splitCache = Mockito.mock(SplitCache.class); + SegmentCache segmentCache = Mockito.mock(SegmentCacheInMemoryImpl.class); + TelemetrySubmitter telemetrySynchronizer = new TelemetrySubmitter(httpClient, URI.create(TELEMETRY_ENDPOINT), consumer, splitCache, segmentCache, telemetryRuntimeProducer, 0l); + return telemetrySynchronizer; + } + + private void populateStats(TelemetryStorage telemetryStorage) { + telemetryStorage.recordLatency(MethodEnum.TREATMENT, 1500l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENT, 2000l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENTS, 3000l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENTS, 500l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENT_WITH_CONFIG, 800l * 1000); + telemetryStorage.recordLatency(MethodEnum.TREATMENTS_WITH_CONFIG, 1000l * 1000); + + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.TELEMETRY, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.TELEMETRY, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.EVENTS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.EVENTS, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SEGMENTS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SPLITS, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SPLITS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.SPLITS, 2000l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.IMPRESSIONS, 1500l * 1000); + telemetryStorage.recordSyncLatency(HTTPLatenciesEnum.IMPRESSIONS_COUNT, 2000l * 1000); + + telemetryStorage.recordException(MethodEnum.TREATMENT); + telemetryStorage.recordException(MethodEnum.TREATMENTS); + telemetryStorage.recordException(MethodEnum.TREATMENT); + telemetryStorage.recordException(MethodEnum.TREATMENTS); + telemetryStorage.recordException(MethodEnum.TREATMENT_WITH_CONFIG); + telemetryStorage.recordException(MethodEnum.TREATMENTS_WITH_CONFIG); + + telemetryStorage.recordAuthRejections(); + + telemetryStorage.recordTokenRefreshes(); + telemetryStorage.recordTokenRefreshes(); + + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, 3); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DEDUPED, 1); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 4); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 6); + telemetryStorage.recordImpressionStats(ImpressionsDataTypeEnum.IMPRESSIONS_DROPPED, 2); + + telemetryStorage.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 3); + telemetryStorage.recordEventStats(EventsDataRecordsEnum.EVENTS_DROPPED, 7); + telemetryStorage.recordEventStats(EventsDataRecordsEnum.EVENTS_QUEUED, 3); + + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.EVENTS, 1500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.EVENTS, 800); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS, 2500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS, 10500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.IMPRESSIONS_COUNT, 1500); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.SEGMENTS, 1580); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.TELEMETRY, 265); + telemetryStorage.recordSuccessfulSync(LastSynchronizationRecordsEnum.TOKEN, 129); + + telemetryStorage.recordSessionLength(91218); + + telemetryStorage.recordSyncError(ResourceEnum.TELEMETRY_SYNC, 400); + telemetryStorage.recordSyncError(ResourceEnum.TELEMETRY_SYNC, 400); + telemetryStorage.recordSyncError(ResourceEnum.SEGMENT_SYNC, 501); + telemetryStorage.recordSyncError(ResourceEnum.IMPRESSION_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.IMPRESSION_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.EVENT_SYNC, 503); + telemetryStorage.recordSyncError(ResourceEnum.SPLIT_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.IMPRESSION_COUNT_SYNC, 403); + telemetryStorage.recordSyncError(ResourceEnum.TOKEN_SYNC, 403); + + StreamingEvent streamingEvent = new StreamingEvent(1, 290, 91218); + telemetryStorage.recordStreamingEvents(streamingEvent); + } + + private void populateConfig(TelemetryStorage telemetryStorage) { + telemetryStorage.recordBURTimeout(); + telemetryStorage.recordBURTimeout(); + telemetryStorage.recordNonReadyUsage(); + telemetryStorage.recordNonReadyUsage(); + telemetryStorage.recordNonReadyUsage(); + } + +} \ No newline at end of file diff --git a/client/src/test/java/io/split/telemetry/synchronizer/TelemetrySyncTaskTest.java b/client/src/test/java/io/split/telemetry/synchronizer/TelemetrySyncTaskTest.java new file mode 100644 index 000000000..055b0f048 --- /dev/null +++ b/client/src/test/java/io/split/telemetry/synchronizer/TelemetrySyncTaskTest.java @@ -0,0 +1,29 @@ +package io.split.telemetry.synchronizer; + +import org.junit.Test; +import org.mockito.Mockito; + +public class TelemetrySyncTaskTest { + + @Test + public void testSynchronizationTask() throws Exception { + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySubmitter.class); + Mockito.doNothing().when(telemetrySynchronizer).synchronizeStats(); + TelemetrySyncTask telemetrySyncTask = new TelemetrySyncTask(1, telemetrySynchronizer); + Thread.sleep(2900); + Mockito.verify(telemetrySynchronizer, Mockito.times(2)).synchronizeStats(); + } + + @Test + public void testStopSynchronizationTask() throws Exception { + TelemetrySynchronizer telemetrySynchronizer = Mockito.mock(TelemetrySubmitter.class); +// Mockito.doNothing().when(telemetrySynchronizer).synchronizeStats(); + TelemetrySyncTask telemetrySyncTask = new TelemetrySyncTask(1, telemetrySynchronizer); + Thread.sleep(2100); + Mockito.verify(telemetrySynchronizer, Mockito.times(2)).synchronizeStats(); + telemetrySyncTask.stopScheduledTask(1l, 1l, 1l); + Mockito.verify(telemetrySynchronizer, Mockito.times(2)).synchronizeStats(); + Mockito.verify(telemetrySynchronizer, Mockito.times(1)).finalSynchronization(1l, 1l, 1l); + } + +} \ No newline at end of file diff --git a/client/src/test/java/io/split/telemetry/utils/AtomicLongArrayTest.java b/client/src/test/java/io/split/telemetry/utils/AtomicLongArrayTest.java new file mode 100644 index 000000000..e5a8a38a0 --- /dev/null +++ b/client/src/test/java/io/split/telemetry/utils/AtomicLongArrayTest.java @@ -0,0 +1,50 @@ +package io.split.telemetry.utils; + +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; +import org.slf4j.Logger; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; + +public class AtomicLongArrayTest { + + private static final int SIZE = 23; + + @Test + public void testAtomicLong() { + AtomicLongArray atomicLongArray = new AtomicLongArray(SIZE); + Assert.assertNotNull(atomicLongArray); + } + + @Test + public void testArraySizeError() { + AtomicLongArray atomicLongArray = new AtomicLongArray(0); + Logger log = Mockito.mock(Logger.class); + atomicLongArray.increment(2); + Assert.assertEquals(1, atomicLongArray.fetchAndClearAll().stream().mapToInt(Long::intValue).sum()); + } + + @Test + public void testIncrement() { + AtomicLongArray atomicLongArray = new AtomicLongArray(SIZE); + atomicLongArray.increment(2); + Assert.assertEquals(1, atomicLongArray.fetchAndClearAll().stream().mapToInt(Long::intValue).sum()); + } + + @Test + public void testIncrementError() throws NoSuchFieldException, IllegalAccessException { + Logger log = Mockito.mock(Logger.class); + AtomicLongArray atomicLongArray = new AtomicLongArray(SIZE); + Field logAssert = AtomicLongArray.class.getDeclaredField("_log"); + logAssert.setAccessible(true); + Field modifiersField = Field.class.getDeclaredField("modifiers"); + modifiersField.setAccessible(true); + modifiersField.setInt(logAssert, logAssert.getModifiers() & ~Modifier.FINAL); + logAssert.set(atomicLongArray, log); + atomicLongArray.increment(25); + Mockito.verify(log, Mockito.times(1)).error(Mockito.anyString()); + } + +} \ No newline at end of file diff --git a/client/src/test/java/io/split/telemetry/utils/BucketCalculatorTest.java b/client/src/test/java/io/split/telemetry/utils/BucketCalculatorTest.java new file mode 100644 index 000000000..04ba74487 --- /dev/null +++ b/client/src/test/java/io/split/telemetry/utils/BucketCalculatorTest.java @@ -0,0 +1,22 @@ +package io.split.telemetry.utils; + +import org.junit.Assert; +import org.junit.Test; + +public class BucketCalculatorTest{ + + @Test + public void testBucketCalculator() { + int bucket = BucketCalculator.getBucketForLatency(500l * 1000); + Assert.assertEquals(0, bucket); + + bucket = BucketCalculator.getBucketForLatency(1500l * 1000); + Assert.assertEquals(1, bucket); + + bucket = BucketCalculator.getBucketForLatency(8000l * 1000); + Assert.assertEquals(6, bucket); + + bucket = BucketCalculator.getBucketForLatency(7481829l * 1000); + Assert.assertEquals(22, bucket); + } +} diff --git a/pom.xml b/pom.xml index 1a40cb3c5..3e2ba35d6 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 io.split.client java-client-parent - 4.1.6 + 4.2.0 diff --git a/testing/pom.xml b/testing/pom.xml index 3d74dc7a8..ea1a844de 100644 --- a/testing/pom.xml +++ b/testing/pom.xml @@ -6,7 +6,7 @@ io.split.client java-client-parent - 4.1.6 + 4.2.0 java-client-testing