diff --git a/api/.gitignore b/api/.gitignore index 796b96d1c..e4dbec6f2 100644 --- a/api/.gitignore +++ b/api/.gitignore @@ -1 +1,3 @@ /build +.classpath +.settings diff --git a/backoff/.gitignore b/backoff/.gitignore index 3a11ced48..6009265cd 100644 --- a/backoff/.gitignore +++ b/backoff/.gitignore @@ -2,3 +2,5 @@ .gradle *.iml .DS_Store +.classpath +.settings diff --git a/build.gradle b/build.gradle index 1abeacb15..0f558a3fe 100644 --- a/build.gradle +++ b/build.gradle @@ -145,7 +145,7 @@ dependencies { return candidates.find { findProject(it) != null } } - ['main', 'logger', 'events', 'events-domain', 'api', 'http-api', 'http', 'fallback', 'backoff', 'tracker'].each { moduleName -> + ['main', 'logger', 'events', 'events-domain', 'api', 'http-api', 'http', 'fallback', 'backoff', 'tracker', 'submitter'].each { moduleName -> def resolvedPath = resolveProjectPath(moduleName) if (resolvedPath != null) { include project(resolvedPath) diff --git a/events-domain/.gitignore b/events-domain/.gitignore index 796b96d1c..e4dbec6f2 100644 --- a/events-domain/.gitignore +++ b/events-domain/.gitignore @@ -1 +1,3 @@ /build +.classpath +.settings diff --git a/events/.gitignore b/events/.gitignore index 42afabfd2..0b60b6351 100644 --- a/events/.gitignore +++ b/events/.gitignore @@ -1 +1,3 @@ -/build \ No newline at end of file +/build +.classpath +.settings \ No newline at end of file diff --git a/fallback/.gitignore b/fallback/.gitignore index 3a11ced48..6009265cd 100644 --- a/fallback/.gitignore +++ b/fallback/.gitignore @@ -2,3 +2,5 @@ .gradle *.iml .DS_Store +.classpath +.settings diff --git a/gradle/common-android-library.gradle b/gradle/common-android-library.gradle index 28bfe3eee..adf1d647d 100644 --- a/gradle/common-android-library.gradle +++ b/gradle/common-android-library.gradle @@ -14,6 +14,10 @@ tasks.withType(JavaCompile).configureEach { options.compilerArgs.add('-parameters') } +tasks.withType(Test).configureEach { + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 +} + def kotlinCompileClass = null try { kotlinCompileClass = Class.forName('org.jetbrains.kotlin.gradle.tasks.KotlinCompile') diff --git a/http-api/.gitignore b/http-api/.gitignore index 796b96d1c..e4dbec6f2 100644 --- a/http-api/.gitignore +++ b/http-api/.gitignore @@ -1 +1,3 @@ /build +.classpath +.settings diff --git a/http/.gitignore b/http/.gitignore index 796b96d1c..e4dbec6f2 100644 --- a/http/.gitignore +++ b/http/.gitignore @@ -1 +1,3 @@ /build +.classpath +.settings diff --git a/logger/.gitignore b/logger/.gitignore index 3a11ced48..6009265cd 100644 --- a/logger/.gitignore +++ b/logger/.gitignore @@ -2,3 +2,5 @@ .gradle *.iml .DS_Store +.classpath +.settings diff --git a/main/.gitignore b/main/.gitignore index 3a11ced48..6009265cd 100644 --- a/main/.gitignore +++ b/main/.gitignore @@ -2,3 +2,5 @@ .gradle *.iml .DS_Store +.classpath +.settings diff --git a/main/build.gradle b/main/build.gradle index a0325264f..e2eb14591 100644 --- a/main/build.gradle +++ b/main/build.gradle @@ -56,6 +56,8 @@ dependencies { api clientModuleProject('fallback') implementation clientModuleProject('backoff') implementation clientModuleProject('tracker') + api clientModuleProject('submitter') + // Internal module dependencies implementation clientModuleProject('http') implementation clientModuleProject('events-domain') diff --git a/main/src/main/java/io/split/android/client/dtos/Event.java b/main/src/main/java/io/split/android/client/dtos/Event.java index fe8c986d9..d10397363 100644 --- a/main/src/main/java/io/split/android/client/dtos/Event.java +++ b/main/src/main/java/io/split/android/client/dtos/Event.java @@ -3,7 +3,7 @@ import com.google.gson.annotations.JsonAdapter; import com.google.gson.annotations.SerializedName; -import io.split.android.client.storage.common.InBytesSizable; +import io.split.android.client.submitter.InBytesSizable; import io.split.android.client.utils.deserializer.EventDeserializer; @JsonAdapter(EventDeserializer.class) diff --git a/main/src/main/java/io/split/android/client/dtos/KeyImpression.java b/main/src/main/java/io/split/android/client/dtos/KeyImpression.java index 8bf7f2e7e..6cd3795e8 100644 --- a/main/src/main/java/io/split/android/client/dtos/KeyImpression.java +++ b/main/src/main/java/io/split/android/client/dtos/KeyImpression.java @@ -6,7 +6,7 @@ import java.util.Objects; import io.split.android.client.service.ServiceConstants; -import io.split.android.client.storage.common.InBytesSizable; +import io.split.android.client.submitter.InBytesSizable; import io.split.android.client.impressions.Impression; public class KeyImpression implements InBytesSizable, Identifiable { diff --git a/main/src/main/java/io/split/android/client/service/HttpRecorderSubmitterAdapter.java b/main/src/main/java/io/split/android/client/service/HttpRecorderSubmitterAdapter.java new file mode 100644 index 000000000..beb51fa7b --- /dev/null +++ b/main/src/main/java/io/split/android/client/service/HttpRecorderSubmitterAdapter.java @@ -0,0 +1,28 @@ +package io.split.android.client.service; + +import androidx.annotation.NonNull; + +import io.split.android.client.service.http.HttpRecorder; +import io.split.android.client.service.http.HttpRecorderException; +import io.split.android.client.service.http.HttpStatus; +import io.split.android.client.submitter.RecorderException; +import io.split.android.client.submitter.RecorderSubmitter; + +public class HttpRecorderSubmitterAdapter implements RecorderSubmitter { + private final HttpRecorder mHttpRecorder; + + public HttpRecorderSubmitterAdapter(@NonNull HttpRecorder httpRecorder) { + mHttpRecorder = httpRecorder; + } + + @Override + public void execute(@NonNull T data) throws RecorderException { + try { + mHttpRecorder.execute(data); + } catch (HttpRecorderException e) { + Integer httpStatus = e.getHttpStatus(); + boolean retryable = !HttpStatus.isNotRetryable(HttpStatus.fromCode(httpStatus)); + throw new RecorderException(e.getMessage(), httpStatus, retryable); + } + } +} diff --git a/main/src/main/java/io/split/android/client/service/TelemetryRecorderAdapter.java b/main/src/main/java/io/split/android/client/service/TelemetryRecorderAdapter.java new file mode 100644 index 000000000..87610860f --- /dev/null +++ b/main/src/main/java/io/split/android/client/service/TelemetryRecorderAdapter.java @@ -0,0 +1,33 @@ +package io.split.android.client.service; + +import androidx.annotation.NonNull; + +import io.split.android.client.submitter.RecorderTelemetry; +import io.split.android.client.telemetry.model.OperationType; +import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; + +public class TelemetryRecorderAdapter implements RecorderTelemetry { + private final TelemetryRuntimeProducer mTelemetryProducer; + private final OperationType mOperationType; + + public TelemetryRecorderAdapter(@NonNull TelemetryRuntimeProducer telemetryProducer, + @NonNull OperationType operationType) { + mTelemetryProducer = telemetryProducer; + mOperationType = operationType; + } + + @Override + public void recordSuccess(long timestamp) { + mTelemetryProducer.recordSuccessfulSync(mOperationType, timestamp); + } + + @Override + public void recordError(Integer httpStatus) { + mTelemetryProducer.recordSyncError(mOperationType, httpStatus); + } + + @Override + public void recordLatency(long latencyMs) { + mTelemetryProducer.recordSyncLatency(mOperationType, latencyMs); + } +} diff --git a/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java b/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java index d380af4e7..fa51ec1e0 100644 --- a/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java +++ b/main/src/main/java/io/split/android/client/service/events/EventsRecorderTask.java @@ -1,116 +1,42 @@ package io.split.android.client.service.events; -import static io.split.android.client.utils.Utils.checkNotNull; -import static io.split.android.client.utils.Utils.partition; - import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import io.split.android.client.dtos.Event; -import io.split.android.client.service.executor.SplitTask; -import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.HttpRecorderSubmitterAdapter; +import io.split.android.client.service.TelemetryRecorderAdapter; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.http.HttpRecorder; -import io.split.android.client.service.http.HttpRecorderException; -import io.split.android.client.service.http.HttpStatus; import io.split.android.client.storage.events.PersistentEventsStorage; +import io.split.android.client.submitter.RecorderTask; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; -import io.split.android.client.utils.logger.Logger; -public class EventsRecorderTask implements SplitTask { - public final static int FAILING_CHUNK_SIZE = 20; - private final PersistentEventsStorage mPersistentEventsStorage; - private final HttpRecorder> mHttpRecorder; - private final EventsRecorderTaskConfig mConfig; - private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; +public class EventsRecorderTask extends RecorderTask> { + + public static final int FAILING_CHUNK_SIZE = 20; public EventsRecorderTask(@NonNull HttpRecorder> httpRecorder, - @NonNull PersistentEventsStorage persistentEventsStorage, + @NonNull PersistentEventsStorage storage, @NonNull EventsRecorderTaskConfig config, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - mHttpRecorder = checkNotNull(httpRecorder); - mPersistentEventsStorage = checkNotNull(persistentEventsStorage); - mConfig = checkNotNull(config); - mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + super(storage, + new HttpRecorderSubmitterAdapter<>(httpRecorder), + config.getEventsPerPush(), + SplitTaskType.EVENTS_RECORDER, + new TelemetryRecorderAdapter(telemetryRuntimeProducer, OperationType.EVENTS), + FAILING_CHUNK_SIZE); } @Override - @NonNull - public SplitTaskExecutionInfo execute() { - SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS; - int nonSentRecords = 0; - long nonSentBytes = 0; - List events; - List failingEvents = new ArrayList<>(); - boolean doNotRetry = false; - do { - events = mPersistentEventsStorage.pop(mConfig.getEventsPerPush()); - if (events.size() > 0) { - long startTime = System.currentTimeMillis(); - long latency = 0; - try { - Logger.d("Posting %d Split events", events.size()); - mHttpRecorder.execute(events); - - long now = System.currentTimeMillis(); - latency = now - startTime; - mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.EVENTS, now); - - mPersistentEventsStorage.delete(events); - Logger.d("%d split events sent", events.size()); - } catch (HttpRecorderException e) { - status = SplitTaskExecutionStatus.ERROR; - nonSentRecords += mConfig.getEventsPerPush(); - nonSentBytes += sumEventBytes(events); - Logger.e("Event recorder task: Some events couldn't be sent" + - "Saving to send them in a new iteration: " + - e.getLocalizedMessage()); - failingEvents.addAll(events); - - mTelemetryRuntimeProducer.recordSyncError(OperationType.EVENTS, e.getHttpStatus()); - - if (HttpStatus.isNotRetryable(e.getHttpStatus())) { - doNotRetry = true; - break; - } - } finally { - mTelemetryRuntimeProducer.recordSyncLatency(OperationType.EVENTS, latency); - } - } - } while (events.size() == mConfig.getEventsPerPush()); - - // Update events by chunks to avoid sqlite errors - List> failingChunks = partition(failingEvents, FAILING_CHUNK_SIZE); - for (List chunk : failingChunks) { - mPersistentEventsStorage.setActive(chunk); - } - - if (status == SplitTaskExecutionStatus.ERROR) { - Map data = new HashMap<>(); - data.put(SplitTaskExecutionInfo.NON_SENT_RECORDS, nonSentRecords); - data.put(SplitTaskExecutionInfo.NON_SENT_BYTES, nonSentBytes); - if (doNotRetry) { - data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true); - } - - return SplitTaskExecutionInfo.error( - SplitTaskType.EVENTS_RECORDER, data); - } - return SplitTaskExecutionInfo.success(SplitTaskType.EVENTS_RECORDER); + protected List transformForSubmission(List items) { + return items; } - private long sumEventBytes(List events) { - long totalBytes = 0; - for (Event event : events) { - totalBytes += event.getSizeInBytes(); - } - return totalBytes; + @Override + protected long estimateItemSize(Event item) { + return item.getSizeInBytes(); } } diff --git a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java index cd761d75b..976d73ef3 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java +++ b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsCountRecorderTask.java @@ -1,96 +1,34 @@ package io.split.android.client.service.impressions; -import static io.split.android.client.utils.Utils.checkNotNull; - import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; +import io.split.android.client.service.HttpRecorderSubmitterAdapter; import io.split.android.client.service.ServiceConstants; -import io.split.android.client.service.executor.SplitTask; -import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.TelemetryRecorderAdapter; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.http.HttpRecorder; -import io.split.android.client.service.http.HttpRecorderException; -import io.split.android.client.service.http.HttpStatus; import io.split.android.client.storage.impressions.PersistentImpressionsCountStorage; +import io.split.android.client.submitter.RecorderTask; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; -import io.split.android.client.utils.logger.Logger; -public class ImpressionsCountRecorderTask implements SplitTask { - private final PersistentImpressionsCountStorage mPersistentStorage; - private final HttpRecorder mHttpRecorder; - private static int POP_COUNT = ServiceConstants.DEFAULT_IMPRESSION_COUNT_ROWS_POP; - private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; +public class ImpressionsCountRecorderTask extends RecorderTask { public ImpressionsCountRecorderTask(@NonNull HttpRecorder httpRecorder, @NonNull PersistentImpressionsCountStorage persistentStorage, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - mHttpRecorder = checkNotNull(httpRecorder); - mPersistentStorage = checkNotNull(persistentStorage); - mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + super(persistentStorage, + new HttpRecorderSubmitterAdapter<>(httpRecorder), + ServiceConstants.DEFAULT_IMPRESSION_COUNT_ROWS_POP, + SplitTaskType.IMPRESSIONS_COUNT_RECORDER, + new TelemetryRecorderAdapter(telemetryRuntimeProducer, OperationType.IMPRESSIONS_COUNT), + 0); } @Override - @NonNull - public SplitTaskExecutionInfo execute() { - SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS; - - List countList = new ArrayList<>(); - List failedSent = new ArrayList<>(); - boolean doNotRetry = false; - do { - countList = mPersistentStorage.pop(POP_COUNT); - if (countList.size() > 0) { - long startTime = System.currentTimeMillis(); - long latency = 0; - try { - Logger.d("Posting %d Split impressions count", countList.size()); - mHttpRecorder.execute(new ImpressionsCount(countList)); - - long now = System.currentTimeMillis(); - latency = now - startTime; - mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.IMPRESSIONS_COUNT, now); - - mPersistentStorage.delete(countList); - Logger.d("%d split impressions count sent", countList.size()); - } catch (HttpRecorderException e) { - status = SplitTaskExecutionStatus.ERROR; - Logger.e("Impressions count recorder task: Some counts couldn't be sent. " + - "Saving to send them in a new iteration\n" + - e.getLocalizedMessage()); - failedSent.addAll(countList); - - mTelemetryRuntimeProducer.recordSyncError(OperationType.IMPRESSIONS_COUNT, e.getHttpStatus()); - - if (HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getHttpStatus()))) { - doNotRetry = true; - break; - } - } finally { - mTelemetryRuntimeProducer.recordSyncLatency(OperationType.IMPRESSIONS_COUNT, latency); - } - } - } while (countList.size() == POP_COUNT); - - if (failedSent.size() > 0) { - mPersistentStorage.setActive(failedSent); - } - - if (status == SplitTaskExecutionStatus.ERROR) { - Map data = new HashMap<>(); - if (doNotRetry) { - data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true); - } - - return SplitTaskExecutionInfo.error(SplitTaskType.IMPRESSIONS_COUNT_RECORDER, data); - } - - return SplitTaskExecutionInfo.success(SplitTaskType.IMPRESSIONS_COUNT_RECORDER); + protected ImpressionsCount transformForSubmission(List items) { + return new ImpressionsCount(items); } } diff --git a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java index 7a4b122c9..57f737b93 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java +++ b/main/src/main/java/io/split/android/client/service/impressions/ImpressionsRecorderTask.java @@ -1,112 +1,43 @@ package io.split.android.client.service.impressions; -import static io.split.android.client.utils.Utils.checkNotNull; - import androidx.annotation.NonNull; -import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import io.split.android.client.dtos.KeyImpression; -import io.split.android.client.service.executor.SplitTask; -import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.HttpRecorderSubmitterAdapter; +import io.split.android.client.service.TelemetryRecorderAdapter; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.http.HttpRecorder; -import io.split.android.client.service.http.HttpRecorderException; -import io.split.android.client.service.http.HttpStatus; import io.split.android.client.storage.impressions.PersistentImpressionsStorage; +import io.split.android.client.submitter.RecorderTask; import io.split.android.client.telemetry.model.OperationType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; -import io.split.android.client.utils.logger.Logger; -public class ImpressionsRecorderTask implements SplitTask { - public final static int FAILING_CHUNK_SIZE = 20; - private final PersistentImpressionsStorage mPersistenImpressionsStorage; - private final HttpRecorder> mHttpRecorder; - private final ImpressionsRecorderTaskConfig mConfig; - private final TelemetryRuntimeProducer mTelemetryRuntimeProducer; +public class ImpressionsRecorderTask extends RecorderTask> { + + private final long mEstimatedSizeInBytes; public ImpressionsRecorderTask(@NonNull HttpRecorder> httpRecorder, - @NonNull PersistentImpressionsStorage persistenEventsStorage, + @NonNull PersistentImpressionsStorage storage, @NonNull ImpressionsRecorderTaskConfig config, @NonNull TelemetryRuntimeProducer telemetryRuntimeProducer) { - mHttpRecorder = checkNotNull(httpRecorder); - mPersistenImpressionsStorage = checkNotNull(persistenEventsStorage); - mConfig = checkNotNull(config); - mTelemetryRuntimeProducer = checkNotNull(telemetryRuntimeProducer); + super(storage, + new HttpRecorderSubmitterAdapter<>(httpRecorder), + config.getImpressionsPerPush(), + SplitTaskType.IMPRESSIONS_RECORDER, + new TelemetryRecorderAdapter(telemetryRuntimeProducer, OperationType.IMPRESSIONS), + 0); + this.mEstimatedSizeInBytes = config.getEstimatedSizeInBytes(); } @Override - @NonNull - public SplitTaskExecutionInfo execute() { - SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS; - int nonSentRecords = 0; - long nonSentBytes = 0; - List impressions; - List failingImpressions = new ArrayList<>(); - boolean doNotRetry = false; - do { - impressions = mPersistenImpressionsStorage.pop(mConfig.getImpressionsPerPush()); - if (impressions.size() > 0) { - long startTime = System.currentTimeMillis(); - long latency = 0; - try { - Logger.d("Posting %d Split impressions", impressions.size()); - mHttpRecorder.execute(impressions); - - long now = System.currentTimeMillis(); - latency = now - startTime; - mTelemetryRuntimeProducer.recordSuccessfulSync(OperationType.IMPRESSIONS, now); - - mPersistenImpressionsStorage.delete(impressions); - Logger.d("%d split impressions sent", impressions.size()); - } catch (HttpRecorderException e) { - status = SplitTaskExecutionStatus.ERROR; - nonSentRecords += mConfig.getImpressionsPerPush(); - nonSentBytes += sumImpressionsBytes(impressions); - Logger.e("Impressions recorder task: Some impressions couldn't be sent. " + - "Saving to send them in a new iteration\n" + - e.getLocalizedMessage()); - failingImpressions.addAll(impressions); - - mTelemetryRuntimeProducer.recordSyncError(OperationType.IMPRESSIONS, e.getHttpStatus()); - - if (HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getHttpStatus()))) { - doNotRetry = true; - break; - } - } finally { - mTelemetryRuntimeProducer.recordSyncLatency(OperationType.IMPRESSIONS, latency); - } - } - } while (impressions.size() == mConfig.getImpressionsPerPush()); - - if (failingImpressions.size() > 0) { - mPersistenImpressionsStorage.setActive(failingImpressions); - } - - if (status == SplitTaskExecutionStatus.ERROR) { - Map data = new HashMap<>(); - data.put(SplitTaskExecutionInfo.NON_SENT_RECORDS, nonSentRecords); - data.put(SplitTaskExecutionInfo.NON_SENT_BYTES, nonSentBytes); - if (doNotRetry) { - data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true); - } - - return SplitTaskExecutionInfo.error( - SplitTaskType.IMPRESSIONS_RECORDER, data); - } - return SplitTaskExecutionInfo.success(SplitTaskType.IMPRESSIONS_RECORDER); + protected List transformForSubmission(List items) { + return items; } - private long sumImpressionsBytes(List impressions) { - long totalBytes = 0; - for (KeyImpression impression : impressions) { - totalBytes += mConfig.getEstimatedSizeInBytes(); - } - return totalBytes; + @Override + protected long estimateItemSize(KeyImpression item) { + return mEstimatedSizeInBytes; } } diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java index 79ea6dc1c..8fe7e3a99 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java +++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugStrategy.java @@ -16,7 +16,7 @@ import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.observer.ImpressionsObserver; -import io.split.android.client.service.synchronizer.RecorderSyncHelper; +import io.split.android.client.submitter.RecorderSyncHelper; import io.split.android.client.telemetry.model.ImpressionsDataType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java index 36d86ff7b..ad1d8219f 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java +++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/DebugTracker.java @@ -15,7 +15,7 @@ import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.observer.ImpressionsObserver; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; -import io.split.android.client.service.synchronizer.RecorderSyncHelper; +import io.split.android.client.submitter.RecorderSyncHelper; class DebugTracker implements PeriodicTracker { diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java index c331ee61f..c76b65dc1 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java +++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/ImpressionStrategyProvider.java @@ -13,7 +13,7 @@ import io.split.android.client.service.impressions.observer.ImpressionsObserverImpl; import io.split.android.client.service.impressions.unique.UniqueKeysTracker; import io.split.android.client.service.impressions.unique.UniqueKeysTrackerImpl; -import io.split.android.client.service.synchronizer.RecorderSyncHelperImpl; +import io.split.android.client.submitter.RecorderSyncHelperImpl; import io.split.android.client.storage.common.SplitStorageContainer; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java index 23f7c4b7c..5c3b85323 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java +++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedStrategy.java @@ -18,7 +18,7 @@ import io.split.android.client.service.impressions.ImpressionsCounter; import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.observer.ImpressionsObserver; -import io.split.android.client.service.synchronizer.RecorderSyncHelper; +import io.split.android.client.submitter.RecorderSyncHelper; import io.split.android.client.telemetry.model.ImpressionsDataType; import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer; diff --git a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java index 5f441dfaf..3ca2bc608 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java +++ b/main/src/main/java/io/split/android/client/service/impressions/strategy/OptimizedTracker.java @@ -15,7 +15,7 @@ import io.split.android.client.service.impressions.ImpressionsTaskFactory; import io.split.android.client.service.impressions.observer.ImpressionsObserver; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; -import io.split.android.client.service.synchronizer.RecorderSyncHelper; +import io.split.android.client.submitter.RecorderSyncHelper; class OptimizedTracker implements PeriodicTracker { diff --git a/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java index a258f16ce..cbafed13f 100644 --- a/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java +++ b/main/src/main/java/io/split/android/client/service/impressions/unique/UniqueKeysRecorderTask.java @@ -1,7 +1,5 @@ package io.split.android.client.service.impressions.unique; -import static io.split.android.client.utils.Utils.checkNotNull; - import androidx.annotation.NonNull; import java.util.ArrayList; @@ -11,111 +9,49 @@ import java.util.Map; import java.util.Set; -import io.split.android.client.service.executor.SplitTask; -import io.split.android.client.service.executor.SplitTaskExecutionInfo; -import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.HttpRecorderSubmitterAdapter; import io.split.android.client.service.executor.SplitTaskType; import io.split.android.client.service.http.HttpRecorder; -import io.split.android.client.service.http.HttpRecorderException; -import io.split.android.client.service.http.HttpStatus; import io.split.android.client.storage.impressions.PersistentImpressionsUniqueStorage; -import io.split.android.client.utils.logger.Logger; +import io.split.android.client.submitter.RecorderTask; -public class UniqueKeysRecorderTask implements SplitTask { +public class UniqueKeysRecorderTask extends RecorderTask { - private final HttpRecorder mHttpRecorder; - private final PersistentImpressionsUniqueStorage mStorage; - private final UniqueKeysRecorderTaskConfig mConfig; + private final long mEstimatedSizeInBytes; public UniqueKeysRecorderTask(@NonNull HttpRecorder uniqueImpressionsRecorder, @NonNull PersistentImpressionsUniqueStorage storage, @NonNull UniqueKeysRecorderTaskConfig config) { - mHttpRecorder = checkNotNull(uniqueImpressionsRecorder); - mStorage = checkNotNull(storage); - mConfig = checkNotNull(config); + super(storage, + new HttpRecorderSubmitterAdapter<>(uniqueImpressionsRecorder), + config.getElementsPerPush(), + SplitTaskType.UNIQUE_KEYS_RECORDER_TASK, + null, + 0); + this.mEstimatedSizeInBytes = config.getEstimatedSizeInBytes(); } - @NonNull @Override - public SplitTaskExecutionInfo execute() { - SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS; - int nonSentRecords = 0; - long nonSentBytes = 0; - List keys; - List failingKeys = new ArrayList<>(); - boolean doNotRetry = false; - do { - keys = mStorage.pop(mConfig.getElementsPerPush()); - if (keys.size() > 0) { - try { - Logger.d("Posting %d Split MTKs", keys.size()); - mHttpRecorder.execute(buildMTK(keys)); - - mStorage.delete(keys); - Logger.d("%d split MTKs sent", keys.size()); - } catch (HttpRecorderException e) { - status = SplitTaskExecutionStatus.ERROR; - nonSentRecords += mConfig.getElementsPerPush(); - nonSentBytes += sumImpressionsBytes(keys); - Logger.e("MTKs recorder task: Some keys couldn't be sent. " + - "Saving to send them in a new iteration\n" + - e.getLocalizedMessage()); - failingKeys.addAll(keys); - - if (HttpStatus.isNotRetryable(HttpStatus.fromCode(e.getHttpStatus()))) { - doNotRetry = true; - break; - } - } - } - } while (keys.size() == mConfig.getElementsPerPush()); - - if (failingKeys.size() > 0) { - mStorage.setActive(failingKeys); - } - - if (status == SplitTaskExecutionStatus.ERROR) { - Map data = new HashMap<>(); - data.put(SplitTaskExecutionInfo.NON_SENT_RECORDS, nonSentRecords); - data.put(SplitTaskExecutionInfo.NON_SENT_BYTES, nonSentBytes); - if (doNotRetry) { - data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true); - } - - return SplitTaskExecutionInfo.error( - SplitTaskType.UNIQUE_KEYS_RECORDER_TASK, data); - } - - return SplitTaskExecutionInfo.success(SplitTaskType.UNIQUE_KEYS_RECORDER_TASK); - } - - @NonNull - private static MTK buildMTK(List keys) { + protected MTK transformForSubmission(List items) { Map map = new HashMap<>(); - for (UniqueKey key : keys) { + for (UniqueKey key : items) { String userKey = key.getKey(); if (!map.containsKey(userKey)) { map.put(userKey, new UniqueKey(userKey, new HashSet<>())); } - UniqueKey uniqueKey = map.get(userKey); if (uniqueKey != null) { Set originalFeatures = uniqueKey.getFeatures(); Set newFeatures = key.getFeatures(); newFeatures.addAll(originalFeatures); - map.put(userKey, new UniqueKey(userKey, newFeatures)); } } - return new MTK(new ArrayList<>(map.values())); } - private long sumImpressionsBytes(List keys) { - long totalBytes = 0; - for (UniqueKey key : keys) { - totalBytes += mConfig.getEstimatedSizeInBytes(); - } - return totalBytes; + @Override + protected long estimateItemSize(UniqueKey item) { + return mEstimatedSizeInBytes; } } diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java index abf55e7fe..51d6feaa4 100644 --- a/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java +++ b/main/src/main/java/io/split/android/client/service/synchronizer/SynchronizerImpl.java @@ -30,7 +30,9 @@ import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistry; import io.split.android.client.service.synchronizer.mysegments.MySegmentsSynchronizerRegistryImpl; import io.split.android.client.shared.UserConsent; -import io.split.android.client.storage.common.StoragePusher; +import io.split.android.client.submitter.RecorderSyncHelper; +import io.split.android.client.submitter.RecorderSyncHelperImpl; +import io.split.android.client.submitter.StoragePusher; import io.split.android.client.storage.splits.SplitsStorage; import io.split.android.client.telemetry.model.EventsDataRecordsEnum; import io.split.android.client.telemetry.model.streaming.SyncModeUpdateStreamingEvent; diff --git a/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java b/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java index c111303d1..21744d98f 100644 --- a/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java +++ b/main/src/main/java/io/split/android/client/storage/common/PersistentStorage.java @@ -4,16 +4,15 @@ import java.util.List; -public interface PersistentStorage extends StoragePusher { +import io.split.android.client.submitter.RecorderStorage; +import io.split.android.client.submitter.StoragePusher; + +public interface PersistentStorage extends StoragePusher, RecorderStorage { // Push method is defined in StoragePusher interface void pushMany(@NonNull List elements); - List pop(int count); - - void setActive(@NonNull List elements); - - void delete(@NonNull List elements); + // pop, delete, and setActive are inherited from RecorderStorage void deleteInvalid(long maxTimestamp); } diff --git a/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java b/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java index 1eb2d1ae7..5da50977a 100644 --- a/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java +++ b/main/src/main/java/io/split/android/client/storage/events/EventsStorage.java @@ -11,7 +11,7 @@ import io.split.android.client.dtos.Event; import io.split.android.client.storage.common.Storage; -import io.split.android.client.storage.common.StoragePusher; +import io.split.android.client.submitter.StoragePusher; import io.split.android.client.utils.logger.Logger; public class EventsStorage implements Storage, StoragePusher { diff --git a/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java b/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java index 403fbe220..188e3d2db 100644 --- a/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java +++ b/main/src/main/java/io/split/android/client/storage/impressions/ImpressionsStorage.java @@ -12,7 +12,7 @@ import io.split.android.client.dtos.KeyImpression; import io.split.android.client.storage.common.PersistentStorage; import io.split.android.client.storage.common.Storage; -import io.split.android.client.storage.common.StoragePusher; +import io.split.android.client.submitter.StoragePusher; import io.split.android.client.utils.logger.Logger; public class ImpressionsStorage implements Storage, StoragePusher { diff --git a/main/src/test/java/io/split/android/client/service/SynchronizerTest.java b/main/src/test/java/io/split/android/client/service/SynchronizerTest.java index 6a5e4aa15..db14dcc2b 100644 --- a/main/src/test/java/io/split/android/client/service/SynchronizerTest.java +++ b/main/src/test/java/io/split/android/client/service/SynchronizerTest.java @@ -71,7 +71,7 @@ import io.split.android.client.service.splits.SplitsUpdateTask; import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer; import io.split.android.client.service.synchronizer.FeatureFlagsSynchronizer; -import io.split.android.client.service.synchronizer.RecorderSyncHelper; +import io.split.android.client.submitter.RecorderSyncHelper; import io.split.android.client.service.synchronizer.SynchronizerImpl; import io.split.android.client.service.synchronizer.WorkManagerWrapper; import io.split.android.client.service.synchronizer.attributes.AttributesSynchronizer; diff --git a/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt index 333a16b8d..c6c1d0bf9 100644 --- a/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt +++ b/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugStrategyTest.kt @@ -8,7 +8,7 @@ import io.split.android.client.service.executor.SplitTaskType import io.split.android.client.service.impressions.ImpressionsRecorderTask import io.split.android.client.service.impressions.ImpressionsTaskFactory import io.split.android.client.service.impressions.observer.ImpressionsObserver -import io.split.android.client.service.synchronizer.RecorderSyncHelper +import io.split.android.client.submitter.RecorderSyncHelper import io.split.android.client.telemetry.model.ImpressionsDataType import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer import org.junit.Before diff --git a/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt index 6ddca42f7..66e17142c 100644 --- a/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt +++ b/main/src/test/java/io/split/android/client/service/impressions/strategy/DebugTrackerTest.kt @@ -9,7 +9,7 @@ import io.split.android.client.service.impressions.ImpressionsRecorderTask import io.split.android.client.service.impressions.ImpressionsTaskFactory import io.split.android.client.service.impressions.observer.ImpressionsObserver import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer -import io.split.android.client.service.synchronizer.RecorderSyncHelper +import io.split.android.client.submitter.RecorderSyncHelper import org.junit.Before import org.junit.Test import org.mockito.ArgumentCaptor diff --git a/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt index 198e4d43b..ddd5967c8 100644 --- a/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt +++ b/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedStrategyTest.kt @@ -11,7 +11,7 @@ import io.split.android.client.service.impressions.ImpressionsCounter import io.split.android.client.service.impressions.ImpressionsRecorderTask import io.split.android.client.service.impressions.ImpressionsTaskFactory import io.split.android.client.service.impressions.observer.ImpressionsObserverImpl -import io.split.android.client.service.synchronizer.RecorderSyncHelper +import io.split.android.client.submitter.RecorderSyncHelper import io.split.android.client.telemetry.model.ImpressionsDataType import io.split.android.client.telemetry.storage.TelemetryRuntimeProducer import org.junit.Before diff --git a/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt b/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt index 71915b9ca..5bd1c4952 100644 --- a/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt +++ b/main/src/test/java/io/split/android/client/service/impressions/strategy/OptimizedTrackerTest.kt @@ -10,7 +10,7 @@ import io.split.android.client.service.executor.SplitTaskType import io.split.android.client.service.impressions.* import io.split.android.client.service.impressions.observer.ImpressionsObserver import io.split.android.client.service.sseclient.sseclient.RetryBackoffCounterTimer -import io.split.android.client.service.synchronizer.RecorderSyncHelper +import io.split.android.client.submitter.RecorderSyncHelper import org.junit.Before import org.junit.Test import org.mockito.ArgumentCaptor diff --git a/main/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java b/main/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java index 1888bf831..dfdaf24a8 100644 --- a/main/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java +++ b/main/src/test/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImplTest.java @@ -11,7 +11,8 @@ import io.split.android.client.service.executor.SplitTaskExecutionListener; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.storage.common.StoragePusher; +import io.split.android.client.submitter.RecorderSyncHelperImpl; +import io.split.android.client.submitter.StoragePusher; public class RecorderSyncHelperImplTest { diff --git a/settings.gradle b/settings.gradle index 4b3af1a51..22c935f1b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,3 +10,4 @@ include ':events' include ':events-domain' include ':backoff' include ':tracker' +include ':submitter' diff --git a/sonar-project.properties b/sonar-project.properties index 85a95779d..185535a4c 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,8 +3,8 @@ sonar.projectKey=splitio_android-client sonar.projectName=android-client # Path to source directories (multi-module) -# Root project contains modules: api, events-domain, main, events, logger, http-api, http -sonar.sources=api/src/main/java,events-domain/src/main/java,main/src/main/java,events/src/main/java,logger/src/main/java,http-api/src/main/java,http/src/main/java +# Root project contains modules: api, events-domain, main, events, logger, http-api, http, fallback, backoff, tracker, submitter +sonar.sources=api/src/main/java,events-domain/src/main/java,main/src/main/java,events/src/main/java,logger/src/main/java,http-api/src/main/java,http/src/main/java,fallback/src/main/java,backoff/src/main/java,tracker/src/main/java,submitter/src/main/java # Path to compiled classes (multi-module) # Include binary paths for all modules: api, events-domain, main, events, logger, http-api, http @@ -15,7 +15,11 @@ sonar.java.binaries=\ events/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ logger/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ http-api/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ - http/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes + http/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + fallback/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + backoff/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + tracker/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes,\ + submitter/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes # Path to dependency/libraries jars (multi-module) sonar.java.libraries=\ @@ -46,11 +50,27 @@ sonar.java.libraries=\ http/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ http/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ http/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ - http/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar + http/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + fallback/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + fallback/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + fallback/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + fallback/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + backoff/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + backoff/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + backoff/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + backoff/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + tracker/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + tracker/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + tracker/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + tracker/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar,\ + submitter/build/intermediates/compile_library_classes_jar/debug/bundleLibCompileToJarDebug/classes.jar,\ + submitter/build/intermediates/runtime_library_classes_jar/debug/bundleLibRuntimeToJarDebug/classes.jar,\ + submitter/build/intermediates/compile_r_class_jar/debug/generateDebugRFile/R.jar,\ + submitter/build/intermediates/compile_and_runtime_r_class_jar/debugUnitTest/generateDebugUnitTestStubRFile/R.jar # Path to test directories (multi-module) # Only include test source folders that are guaranteed to exist in all environments -sonar.tests=api/src/test/java,events-domain/src/test/java,main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java,http-api/src/test/java,http/src/test/java +sonar.tests=api/src/test/java,events-domain/src/test/java,main/src/test/java,main/src/androidTest/java,main/src/sharedTest/java,events/src/test/java,logger/src/test/java,http-api/src/test/java,http/src/test/java,fallback/src/test/java,backoff/src/test/java,tracker/src/test/java,submitter/src/test/java # Encoding of the source code sonar.sourceEncoding=UTF-8 diff --git a/submitter/.gitignore b/submitter/.gitignore new file mode 100644 index 000000000..e4dbec6f2 --- /dev/null +++ b/submitter/.gitignore @@ -0,0 +1,3 @@ +/build +.classpath +.settings diff --git a/submitter/README.md b/submitter/README.md new file mode 100644 index 000000000..5735f8783 --- /dev/null +++ b/submitter/README.md @@ -0,0 +1,12 @@ +# submitter + +Generic batch recorder task abstraction. + +## Purpose + +Encapsulates the logic for submitting batched data (such as impressions and events) to the backend. It provides a reusable abstraction for recorder tasks, decoupled from the SDK's internal storage and networking layers. Dependencies are injected via callbacks. + +## Design notes + +- For now depends on `events-domain` for the executor types. +- Depends on `logger` for logging. diff --git a/submitter/build.gradle b/submitter/build.gradle new file mode 100644 index 000000000..906878de7 --- /dev/null +++ b/submitter/build.gradle @@ -0,0 +1,23 @@ +plugins { + id 'com.android.library' +} + +apply from: "$projectDir/../gradle/common-android-library.gradle" + +android { + namespace 'io.split.android.client.submitter' + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + api clientModuleProject('events-domain') + implementation clientModuleProject('logger') + implementation libs.annotation + + testImplementation libs.junit4 + testImplementation libs.mockitoCore +} diff --git a/submitter/src/main/AndroidManifest.xml b/submitter/src/main/AndroidManifest.xml new file mode 100644 index 000000000..9a40236b9 --- /dev/null +++ b/submitter/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + + diff --git a/main/src/main/java/io/split/android/client/storage/common/InBytesSizable.java b/submitter/src/main/java/io/split/android/client/submitter/InBytesSizable.java similarity index 57% rename from main/src/main/java/io/split/android/client/storage/common/InBytesSizable.java rename to submitter/src/main/java/io/split/android/client/submitter/InBytesSizable.java index 645224611..6cb1fbcd1 100644 --- a/main/src/main/java/io/split/android/client/storage/common/InBytesSizable.java +++ b/submitter/src/main/java/io/split/android/client/submitter/InBytesSizable.java @@ -1,4 +1,4 @@ -package io.split.android.client.storage.common; +package io.split.android.client.submitter; public interface InBytesSizable { long getSizeInBytes(); diff --git a/submitter/src/main/java/io/split/android/client/submitter/RecorderException.java b/submitter/src/main/java/io/split/android/client/submitter/RecorderException.java new file mode 100644 index 000000000..31e49ffcb --- /dev/null +++ b/submitter/src/main/java/io/split/android/client/submitter/RecorderException.java @@ -0,0 +1,20 @@ +package io.split.android.client.submitter; + +public class RecorderException extends Exception { + private final Integer mHttpStatus; + private final boolean mRetryable; + + public RecorderException(String message, Integer httpStatus, boolean retryable) { + super(message); + this.mHttpStatus = httpStatus; + this.mRetryable = retryable; + } + + public Integer getHttpStatus() { + return mHttpStatus; + } + + public boolean isRetryable() { + return mRetryable; + } +} diff --git a/submitter/src/main/java/io/split/android/client/submitter/RecorderStorage.java b/submitter/src/main/java/io/split/android/client/submitter/RecorderStorage.java new file mode 100644 index 000000000..350190c54 --- /dev/null +++ b/submitter/src/main/java/io/split/android/client/submitter/RecorderStorage.java @@ -0,0 +1,10 @@ +package io.split.android.client.submitter; + +import androidx.annotation.NonNull; +import java.util.List; + +public interface RecorderStorage { + List pop(int count); + void delete(@NonNull List items); + void setActive(@NonNull List items); +} diff --git a/submitter/src/main/java/io/split/android/client/submitter/RecorderSubmitter.java b/submitter/src/main/java/io/split/android/client/submitter/RecorderSubmitter.java new file mode 100644 index 000000000..9ea278617 --- /dev/null +++ b/submitter/src/main/java/io/split/android/client/submitter/RecorderSubmitter.java @@ -0,0 +1,7 @@ +package io.split.android.client.submitter; + +import androidx.annotation.NonNull; + +public interface RecorderSubmitter { + void execute(@NonNull T data) throws RecorderException; +} diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelper.java b/submitter/src/main/java/io/split/android/client/submitter/RecorderSyncHelper.java similarity index 75% rename from main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelper.java rename to submitter/src/main/java/io/split/android/client/submitter/RecorderSyncHelper.java index fc84c75e4..fd70cba57 100644 --- a/main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelper.java +++ b/submitter/src/main/java/io/split/android/client/submitter/RecorderSyncHelper.java @@ -1,7 +1,6 @@ -package io.split.android.client.service.synchronizer; +package io.split.android.client.submitter; import io.split.android.client.service.executor.SplitTaskExecutionListener; -import io.split.android.client.storage.common.InBytesSizable; public interface RecorderSyncHelper extends SplitTaskExecutionListener { boolean pushAndCheckIfFlushNeeded(T entity); diff --git a/main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java b/submitter/src/main/java/io/split/android/client/submitter/RecorderSyncHelperImpl.java similarity index 90% rename from main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java rename to submitter/src/main/java/io/split/android/client/submitter/RecorderSyncHelperImpl.java index 4351930fb..09488a69e 100644 --- a/main/src/main/java/io/split/android/client/service/synchronizer/RecorderSyncHelperImpl.java +++ b/submitter/src/main/java/io/split/android/client/submitter/RecorderSyncHelperImpl.java @@ -1,11 +1,10 @@ -package io.split.android.client.service.synchronizer; - -import static io.split.android.client.utils.Utils.checkNotNull; +package io.split.android.client.submitter; import androidx.annotation.NonNull; import java.lang.ref.WeakReference; import java.util.HashSet; +import java.util.Objects; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLong; @@ -16,8 +15,6 @@ import io.split.android.client.service.executor.SplitTaskExecutionStatus; import io.split.android.client.service.executor.SplitTaskExecutor; import io.split.android.client.service.executor.SplitTaskType; -import io.split.android.client.storage.common.InBytesSizable; -import io.split.android.client.storage.common.StoragePusher; public class RecorderSyncHelperImpl implements RecorderSyncHelper { @@ -35,9 +32,9 @@ public RecorderSyncHelperImpl(SplitTaskType taskType, int maxQueueSize, long maxQueueSizeInBytes, SplitTaskExecutor splitTaskExecutor) { - mTaskType = checkNotNull(taskType); - mStorage = checkNotNull(storage); - mSplitTaskExecutor = checkNotNull(splitTaskExecutor); + mTaskType = Objects.requireNonNull(taskType); + mStorage = Objects.requireNonNull(storage); + mSplitTaskExecutor = Objects.requireNonNull(splitTaskExecutor); mPushedCount = new AtomicInteger(0); mTotalPushedSizeInBytes = new AtomicLong(0); mMaxQueueSize = maxQueueSize; diff --git a/submitter/src/main/java/io/split/android/client/submitter/RecorderTask.java b/submitter/src/main/java/io/split/android/client/submitter/RecorderTask.java new file mode 100644 index 000000000..5a822812e --- /dev/null +++ b/submitter/src/main/java/io/split/android/client/submitter/RecorderTask.java @@ -0,0 +1,145 @@ +package io.split.android.client.submitter; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.split.android.client.service.executor.SplitTask; +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.executor.SplitTaskType; +import io.split.android.client.utils.logger.Logger; + +/** + * Abstract base class for batch submission tasks. + *

+ * Encapsulates the common pop-submit-retry-setActive pattern used by + * impressions, events, and other batch recorder tasks. + * + * @param The storage item type (e.g., KeyImpression, Event) + * @param The submission payload type (e.g., List, ImpressionsCount) + */ +public abstract class RecorderTask implements SplitTask { + + private final RecorderStorage mStorage; + private final RecorderSubmitter mSubmitter; + private final int mBatchSize; + private final SplitTaskType mTaskType; + @Nullable + private final RecorderTelemetry mTelemetry; + private final int mFailingChunkSize; // 0 = no chunking + + protected RecorderTask(@NonNull RecorderStorage storage, + @NonNull RecorderSubmitter submitter, + int batchSize, + @NonNull SplitTaskType taskType, + @Nullable RecorderTelemetry telemetry, + int failingChunkSize) { + mStorage = storage; + mSubmitter = submitter; + mBatchSize = batchSize; + mTaskType = taskType; + mTelemetry = telemetry; + mFailingChunkSize = failingChunkSize; + } + + @NonNull + @Override + public final SplitTaskExecutionInfo execute() { + SplitTaskExecutionStatus status = SplitTaskExecutionStatus.SUCCESS; + int nonSentRecords = 0; + long nonSentBytes = 0; + List items; + List failingItems = new ArrayList<>(); + boolean doNotRetry = false; + + do { + items = mStorage.pop(mBatchSize); + if (!items.isEmpty()) { + long startTime = System.currentTimeMillis(); + try { + R payload = transformForSubmission(items); + mSubmitter.execute(payload); + + long now = System.currentTimeMillis(); + if (mTelemetry != null) { + mTelemetry.recordSuccess(now); + } + + mStorage.delete(items); + } catch (RecorderException e) { + status = SplitTaskExecutionStatus.ERROR; + nonSentRecords += items.size(); + nonSentBytes += sumBytes(items); + Logger.e("RecorderTask: " + items.size() + " items couldn't be submitted. " + + "Saving to retry in a new iteration: " + e.getLocalizedMessage()); + failingItems.addAll(items); + + if (mTelemetry != null) { + mTelemetry.recordError(e.getHttpStatus()); + } + + if (!e.isRetryable()) { + doNotRetry = true; + break; + } + } finally { + if (mTelemetry != null) { + mTelemetry.recordLatency(System.currentTimeMillis() - startTime); + } + } + } + } while (items.size() == mBatchSize); + + // Re-queue failed items for retry + if (!failingItems.isEmpty()) { + if (mFailingChunkSize > 0) { + // Chunk to avoid SQLite errors (used by EventsRecorderTask) + int size = failingItems.size(); + for (int i = 0; i < size; i += mFailingChunkSize) { + mStorage.setActive(failingItems.subList(i, Math.min(i + mFailingChunkSize, size))); + } + } else { + mStorage.setActive(failingItems); + } + } + + if (status == SplitTaskExecutionStatus.ERROR) { + Map data = new HashMap<>(); + data.put(SplitTaskExecutionInfo.NON_SENT_RECORDS, nonSentRecords); + data.put(SplitTaskExecutionInfo.NON_SENT_BYTES, nonSentBytes); + if (doNotRetry) { + data.put(SplitTaskExecutionInfo.DO_NOT_RETRY, true); + } + return SplitTaskExecutionInfo.error(mTaskType, data); + } + + return SplitTaskExecutionInfo.success(mTaskType); + } + + /** + * Transform storage items into the submission payload before submitting. + */ + protected abstract R transformForSubmission(List items); + + /** + * Estimate the byte size of one storage item for tracking non-sent bytes. + *

+ * Default returns 0. Override to enable byte tracking. + */ + protected long estimateItemSize(T item) { + return 0; + } + + private long sumBytes(List items) { + long total = 0; + for (T item : items) { + total += estimateItemSize(item); + } + return total; + } +} diff --git a/submitter/src/main/java/io/split/android/client/submitter/RecorderTelemetry.java b/submitter/src/main/java/io/split/android/client/submitter/RecorderTelemetry.java new file mode 100644 index 000000000..7f8665ab2 --- /dev/null +++ b/submitter/src/main/java/io/split/android/client/submitter/RecorderTelemetry.java @@ -0,0 +1,7 @@ +package io.split.android.client.submitter; + +public interface RecorderTelemetry { + void recordSuccess(long timestamp); + void recordError(Integer httpStatus); + void recordLatency(long latencyMs); +} diff --git a/main/src/main/java/io/split/android/client/storage/common/StoragePusher.java b/submitter/src/main/java/io/split/android/client/submitter/StoragePusher.java similarity index 69% rename from main/src/main/java/io/split/android/client/storage/common/StoragePusher.java rename to submitter/src/main/java/io/split/android/client/submitter/StoragePusher.java index aa8e6cd6d..1ec49d849 100644 --- a/main/src/main/java/io/split/android/client/storage/common/StoragePusher.java +++ b/submitter/src/main/java/io/split/android/client/submitter/StoragePusher.java @@ -1,4 +1,4 @@ -package io.split.android.client.storage.common; +package io.split.android.client.submitter; import androidx.annotation.NonNull; diff --git a/submitter/src/test/java/io/split/android/client/submitter/RecorderTaskTest.java b/submitter/src/test/java/io/split/android/client/submitter/RecorderTaskTest.java new file mode 100644 index 000000000..1e0994e6e --- /dev/null +++ b/submitter/src/test/java/io/split/android/client/submitter/RecorderTaskTest.java @@ -0,0 +1,546 @@ +package io.split.android.client.submitter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import androidx.annotation.NonNull; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +import java.util.ArrayList; +import java.util.List; + +import io.split.android.client.service.executor.SplitTaskExecutionInfo; +import io.split.android.client.service.executor.SplitTaskExecutionStatus; +import io.split.android.client.service.executor.SplitTaskType; + +@SuppressWarnings("unchecked") +public class RecorderTaskTest { + + private static final int BATCH_SIZE = 10; + private static final SplitTaskType TASK_TYPE = SplitTaskType.IMPRESSIONS_RECORDER; + + private RecorderStorage mStorage; + private RecorderSubmitter> mSubmitter; + private RecorderTelemetry mTelemetry; + + @Before + public void setUp() { + mStorage = Mockito.mock(RecorderStorage.class); + mSubmitter = Mockito.mock(RecorderSubmitter.class); + mTelemetry = Mockito.mock(RecorderTelemetry.class); + } + + // region Successful submission + + @Test + public void successfulSingleBatchSubmission() throws RecorderException { + List batch = createItems(5); // less than BATCH_SIZE → loop terminates after one iteration + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + verify(mStorage, times(1)).pop(BATCH_SIZE); + verify(mSubmitter, times(1)).execute(batch); + verify(mStorage, times(1)).delete(batch); + verify(mStorage, never()).setActive(any()); + assertEquals(TASK_TYPE, result.getTaskType()); + assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + assertNull(result.getIntegerValue(SplitTaskExecutionInfo.NON_SENT_RECORDS)); + assertNull(result.getLongValue(SplitTaskExecutionInfo.NON_SENT_BYTES)); + assertNull(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); + } + + @Test + public void successfulMultiBatchSubmissionLoopsUntilSmallBatch() throws RecorderException { + List fullBatch = createItems(BATCH_SIZE); + List partialBatch = createItems(3); + + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(fullBatch) + .thenReturn(fullBatch) + .thenReturn(partialBatch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + // Three pops: full, full, partial (terminates) + verify(mStorage, times(3)).pop(BATCH_SIZE); + verify(mSubmitter, times(3)).execute(any()); + verify(mStorage, times(3)).delete(any()); + verify(mStorage, never()).setActive(any()); + assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + } + + @Test + public void emptyFirstPopSkipsSubmissionAndSucceeds() throws RecorderException { + when(mStorage.pop(BATCH_SIZE)).thenReturn(new ArrayList<>()); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + verify(mStorage, times(1)).pop(BATCH_SIZE); + verify(mSubmitter, never()).execute(any()); + verify(mStorage, never()).delete(any()); + verify(mStorage, never()).setActive(any()); + assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + } + + // endregion + + // region Error handling + + @Test + public void retryableErrorCollectsFailuresAndContinuesLoop() throws RecorderException { + List batch = createItems(BATCH_SIZE); + List partialBatch = createItems(3); + + // First pop returns a full batch (fails), second also returns full (succeeds), + // third returns partial (terminates) + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(batch) + .thenReturn(partialBatch); + + // Throw only on the first call to execute; subsequent calls succeed + doThrow(new RecorderException("retryable error", 500, true)) + .doNothing() + .doNothing() + .when(mSubmitter).execute(any()); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + // Three pops total: two full batches + one partial + verify(mStorage, times(3)).pop(BATCH_SIZE); + // First batch failed → not deleted; second and partial → deleted twice + verify(mStorage, times(2)).delete(any()); + // Failing items (one batch worth) are re-queued + verify(mStorage, times(1)).setActive(any()); + + assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); + assertEquals(Integer.valueOf(BATCH_SIZE), result.getIntegerValue(SplitTaskExecutionInfo.NON_SENT_RECORDS)); + } + + @Test + public void retryableErrorPopulatesNonSentRecordsCount() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + assertEquals(Integer.valueOf(BATCH_SIZE), result.getIntegerValue(SplitTaskExecutionInfo.NON_SENT_RECORDS)); + assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); + } + + @Test + public void nonRetryableErrorStopsLoopImmediately() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(batch); // Would be popped if loop continued + doThrow(new RecorderException("non-retryable", 9009, false)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + // Only one pop — loop broke immediately on non-retryable error + verify(mStorage, times(1)).pop(BATCH_SIZE); + verify(mStorage, never()).delete(any()); + verify(mStorage, times(1)).setActive(any()); + + assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); + assertTrue(Boolean.TRUE.equals(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY))); + } + + @Test + public void retryableErrorDoesNotSetDoNotRetry() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + assertNull(result.getBoolValue(SplitTaskExecutionInfo.DO_NOT_RETRY)); + } + + // endregion + + // region setActive + + @Test + public void setActiveIsCalledWithFailedItemsOnError() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mStorage, times(1)).setActive(batch); + } + + @Test + public void setActiveIsNotCalledOnSuccess() throws RecorderException { + List batch = createItems(3); + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mStorage, never()).setActive(any()); + } + + @Test + public void chunkedSetActiveWhenFailingChunkSizeIsPositive() throws RecorderException { + int failingChunkSize = 3; + // Create items whose count is a multiple of failingChunkSize for predictable verification + List batch = createItems(9); // 9 items / chunkSize 3 = 3 setActive calls + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, failingChunkSize); + task.execute(); + + // 9 items chunked into 3 → 3 setActive calls + verify(mStorage, times(3)).setActive(any()); + } + + @Test + public void chunkedSetActiveHandlesNonEvenDivision() throws RecorderException { + int failingChunkSize = 3; + // 10 items / chunkSize 3 = 4 calls (chunks of 3, 3, 3, 1) + List batch = createItems(10); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, failingChunkSize); + task.execute(); + + verify(mStorage, times(4)).setActive(any()); + } + + @Test + public void noChunkingWhenFailingChunkSizeIsZero() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + // No chunking → exactly one setActive call with all failing items + verify(mStorage, times(1)).setActive(batch); + } + + // endregion + + // region Byte tracking via estimateItemSize + + @Test + public void byteTrackingViaEstimateItemSizeOverride() throws RecorderException { + long itemSizeBytes = 50L; + List batch = createItems(BATCH_SIZE); // 10 items * 50 bytes = 500 bytes + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTaskWithItemSize(BATCH_SIZE, TASK_TYPE, mTelemetry, 0, itemSizeBytes); + SplitTaskExecutionInfo result = task.execute(); + + long expectedBytes = BATCH_SIZE * itemSizeBytes; + assertEquals(Long.valueOf(expectedBytes), result.getLongValue(SplitTaskExecutionInfo.NON_SENT_BYTES)); + } + + @Test + public void byteTrackingDefaultsToZeroWhenNotOverridden() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + // Default estimateItemSize returns 0 → nonSentBytes = 0 + assertEquals(Long.valueOf(0L), result.getLongValue(SplitTaskExecutionInfo.NON_SENT_BYTES)); + } + + // endregion + + // region transformForSubmission + + @Test + public void transformForSubmissionHookIsApplied() throws RecorderException { + List batch = createItems(3); + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + // Build a task with a custom transform that wraps items in a new list + RecorderTask> task = new SimpleRecorderTask( + mStorage, mSubmitter, BATCH_SIZE, TASK_TYPE, mTelemetry, 0) { + @Override + protected List transformForSubmission(List items) { + List transformed = new ArrayList<>(); + for (String item : items) { + transformed.add(item.toUpperCase()); + } + return transformed; + } + }; + task.execute(); + + // The submitter should receive the transformed list (all uppercase) + List expectedTransformed = new ArrayList<>(); + for (String item : batch) { + expectedTransformed.add(item.toUpperCase()); + } + verify(mSubmitter, times(1)).execute(expectedTransformed); + } + + // endregion + + // region Null telemetry + + @Test + public void nullTelemetryDoesNotThrowNpeOnSuccess() throws RecorderException { + List batch = createItems(3); + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, null, 0); + // Should not throw + SplitTaskExecutionInfo result = task.execute(); + + assertEquals(SplitTaskExecutionStatus.SUCCESS, result.getStatus()); + } + + @Test + public void nullTelemetryDoesNotThrowNpeOnError() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, null, 0); + // Should not throw + SplitTaskExecutionInfo result = task.execute(); + + assertEquals(SplitTaskExecutionStatus.ERROR, result.getStatus()); + } + + // endregion + + // region Telemetry interactions + + @Test + public void telemetryRecordSuccessCalledOnSuccessfulSubmission() throws RecorderException { + List batch = createItems(3); + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mTelemetry, times(1)).recordSuccess(anyLong()); + } + + @Test + public void telemetryRecordSuccessCalledOncePerBatch() throws RecorderException { + List fullBatch = createItems(BATCH_SIZE); + List partialBatch = createItems(3); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(fullBatch) + .thenReturn(partialBatch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mTelemetry, times(2)).recordSuccess(anyLong()); + } + + @Test + public void telemetryRecordLatencyCalledOnSuccess() throws RecorderException { + List batch = createItems(3); + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mTelemetry, atLeastOnce()).recordLatency(anyLong()); + } + + @Test + public void telemetryRecordLatencyCalledOnError() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mTelemetry, atLeastOnce()).recordLatency(anyLong()); + } + + @Test + public void telemetryRecordErrorCalledWithHttpStatusOnError() throws RecorderException { + int httpStatus = 500; + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", httpStatus, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mTelemetry, times(1)).recordError(httpStatus); + } + + @Test + public void telemetryRecordSuccessNotCalledOnError() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mTelemetry, never()).recordSuccess(anyLong()); + } + + @Test + public void telemetryRecordErrorNotCalledOnSuccess() throws RecorderException { + List batch = createItems(3); + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + task.execute(); + + verify(mTelemetry, never()).recordError(any(Integer.class)); + } + + // endregion + + // region Task type + + @Test + public void taskTypeIsPreservedInSuccessResult() throws RecorderException { + List batch = createItems(3); + when(mStorage.pop(BATCH_SIZE)).thenReturn(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + assertEquals(TASK_TYPE, result.getTaskType()); + } + + @Test + public void taskTypeIsPreservedInErrorResult() throws RecorderException { + List batch = createItems(BATCH_SIZE); + when(mStorage.pop(BATCH_SIZE)) + .thenReturn(batch) + .thenReturn(new ArrayList<>()); + doThrow(new RecorderException("error", 500, true)).when(mSubmitter).execute(batch); + + RecorderTask> task = buildTask(BATCH_SIZE, TASK_TYPE, mTelemetry, 0); + SplitTaskExecutionInfo result = task.execute(); + + assertEquals(TASK_TYPE, result.getTaskType()); + } + + // endregion + + // region Helpers + + private List createItems(int count) { + List items = new ArrayList<>(); + for (int i = 0; i < count; i++) { + items.add("item_" + i); + } + return items; + } + + /** + * Builds a standard {@link RecorderTask} with no custom overrides. + * Uses the default {@link RecorderTask#transformForSubmission} (identity cast) and + * {@link RecorderTask#estimateItemSize} (returns 0). + */ + private RecorderTask> buildTask(int batchSize, + SplitTaskType taskType, + RecorderTelemetry telemetry, + int failingChunkSize) { + return new SimpleRecorderTask(mStorage, mSubmitter, batchSize, taskType, telemetry, failingChunkSize); + } + + /** + * Builds a {@link RecorderTask} with a custom fixed item size returned from + * {@link RecorderTask#estimateItemSize}, to exercise byte tracking. + */ + private RecorderTask> buildTaskWithItemSize(int batchSize, + SplitTaskType taskType, + RecorderTelemetry telemetry, + int failingChunkSize, + long itemSizeBytes) { + return new SimpleRecorderTask(mStorage, mSubmitter, batchSize, taskType, telemetry, failingChunkSize) { + @Override + protected long estimateItemSize(String item) { + return itemSizeBytes; + } + }; + } + + /** + * Minimal concrete subclass of {@link RecorderTask} for testing. + * T = String, R = List<String> (identity transform — same type). + */ + private static class SimpleRecorderTask extends RecorderTask> { + + SimpleRecorderTask(@NonNull RecorderStorage storage, + @NonNull RecorderSubmitter> submitter, + int batchSize, + @NonNull SplitTaskType taskType, + RecorderTelemetry telemetry, + int failingChunkSize) { + super(storage, submitter, batchSize, taskType, telemetry, failingChunkSize); + } + + @Override + protected List transformForSubmission(List items) { + return items; + } + } + + // endregion +} diff --git a/tracker/.gitignore b/tracker/.gitignore index 796b96d1c..e4dbec6f2 100644 --- a/tracker/.gitignore +++ b/tracker/.gitignore @@ -1 +1,3 @@ /build +.classpath +.settings