Bug 1243585 - Add TelemetryDispatcher. r=sebastian
Note: for version control and review simplicity, this does not yet compile.
MozReview-Commit-ID: EvccGtseOKT
new file mode 100644
--- /dev/null
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
@@ -0,0 +1,122 @@
+/*
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+package org.mozilla.gecko.telemetry;
+
+import android.content.Context;
+import android.util.Log;
+import org.mozilla.gecko.telemetry.core.TelemetryCorePingBuilder;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler;
+import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
+import org.mozilla.gecko.telemetry.stores.JSONFilePingStore;
+import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
+import org.mozilla.gecko.util.ThreadUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+
+/**
+ * The entry-point for Java-based telemetry. This class handles:
+ * * Initializing the Stores & Schedulers.
+ * * Queueing upload requests for a given ping.
+ *
+ * The full architecture is:
+ *
+ * Fennec -(PingBuilder)-> Dispatcher -2-> Scheduler -> UploadService
+ * | 1 |
+ * Store <--------------------------
+ *
+ * The store acts as a single store of truth and contains a list of all
+ * pings waiting to be uploaded. The dispatcher will queue a ping to upload
+ * by writing it to the store. Later, the UploadService will try to upload
+ * this queued ping by reading directly from the store.
+ *
+ * To implement a new ping type, you should:
+ * 1) Implement a {@link TelemetryPingBuilder} for your ping type.
+ * 2) Re-use a ping store in .../stores/ or implement a new one: {@link TelemetryPingStore}. The
+ * type of store may be affected by robustness requirements (e.g. do you have data in addition to
+ * pings that need to be atomically updated when a ping is stored?) and performance requirements.
+ * 3) Re-use an upload scheduler in .../schedulers/ or implement a new one: {@link TelemetryUploadScheduler}.
+ * 4) Initialize your Store & (if new) Scheduler in the constructor of this class
+ * 5) Add a queuePingForUpload method for your PingBuilder class (see
+ * {@link #queuePingForUpload(Context, TelemetryCorePingBuilder)})
+ * 6) In Fennec, where you want to store a ping and attempt upload, create a PingBuilder and
+ * pass it to the new queuePingForUpload method.
+ */
+public class TelemetryDispatcher {
+ private static final String LOGTAG = "Gecko" + TelemetryDispatcher.class.getSimpleName();
+
+ private static final String STORE_CONTAINER_DIR_NAME = "telemetry_java";
+ private static final String CORE_STORE_DIR_NAME = "core";
+
+ private final JSONFilePingStore coreStore;
+
+ private final TelemetryUploadAllPingsImmediatelyScheduler uploadAllPingsImmediatelyScheduler;
+
+ public TelemetryDispatcher(final String profilePath) {
+ final String storePath = profilePath + File.separator + STORE_CONTAINER_DIR_NAME;
+
+ // There are measurements in the core ping (e.g. seq #) that would ideally be atomically updated
+ // when the ping is stored. However, for simplicity, we use the json store and accept the possible
+ // loss of data (see bug 1243585 comment 16+ for more).
+ coreStore = new JSONFilePingStore(new File(storePath, CORE_STORE_DIR_NAME));
+
+ uploadAllPingsImmediatelyScheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
+ }
+
+ private void queuePingForUpload(final Context context, final int uniqueID, final TelemetryPing ping,
+ final TelemetryPingStore store, final TelemetryUploadScheduler scheduler) {
+ final QueuePingRunnable runnable = new QueuePingRunnable(context, uniqueID, ping, store, scheduler);
+ ThreadUtils.postToBackgroundThread(runnable); // TODO: Investigate how busy this thread is. See if we want another.
+ }
+
+ /**
+ * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
+ */
+ public void queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder) {
+ final TelemetryPing ping = pingBuilder.build();
+ final int id = ping.getPayload().getIntegerSafely(TelemetryCorePingBuilder.SEQ);
+ queuePingForUpload(context, id, ping, coreStore, uploadAllPingsImmediatelyScheduler);
+ }
+
+ private static class QueuePingRunnable implements Runnable {
+ private final WeakReference<Context> contextWeakReference;
+ private final int uniqueID;
+ private final TelemetryPing ping;
+ private final TelemetryPingStore store;
+ private final TelemetryUploadScheduler scheduler;
+
+ public QueuePingRunnable(final Context context, final int uniqueID, final TelemetryPing ping,
+ final TelemetryPingStore store, final TelemetryUploadScheduler scheduler) {
+ this.contextWeakReference = new WeakReference<>(context);
+ this.uniqueID = uniqueID;
+ this.ping = ping;
+ this.store = store;
+ this.scheduler = scheduler;
+ }
+
+ @Override
+ public void run() {
+ final Context context = contextWeakReference.get();
+ if (context == null) {
+ return;
+ }
+
+ // We block while storing the ping so the scheduled upload is guaranteed to have the newly-stored value.
+ try {
+ store.storePing(uniqueID, ping);
+ } catch (final IOException e) {
+ // Don't log exception to avoid leaking profile path.
+ Log.e(LOGTAG, "Unable to write ping to disk. Continuing with upload attempt");
+ }
+
+ if (scheduler.isReadyToUpload(store)) {
+ scheduler.scheduleUpload(context, store);
+ }
+ }
+ }
+}
--- a/mobile/android/base/java/org/mozilla/gecko/telemetry/core/TelemetryCorePingBuilder.java
+++ b/mobile/android/base/java/org/mozilla/gecko/telemetry/core/TelemetryCorePingBuilder.java
@@ -37,17 +37,17 @@ public class TelemetryCorePingBuilder ex
private static final String DEFAULT_SEARCH_ENGINE = "defaultSearch";
private static final String DEVICE = "device";
private static final String DISTRIBUTION_ID = "distributionId";
private static final String EXPERIMENTS = "experiments";
private static final String LOCALE = "locale";
private static final String OS_ATTR = "os";
private static final String OS_VERSION = "osversion";
private static final String PROFILE_CREATION_DATE = "profileDate";
- private static final String SEQ = "seq";
+ public static final String SEQ = "seq";
private static final String VERSION_ATTR = "v";
public TelemetryCorePingBuilder(final Context context, final String serverURLSchemeHostPort) {
super(serverURLSchemeHostPort);
initPayloadConstants(context);
}
private void initPayloadConstants(final Context context) {
--- a/mobile/android/base/moz.build
+++ b/mobile/android/base/moz.build
@@ -570,16 +570,17 @@ gbjar.sources += ['java/org/mozilla/geck
'tabs/TabsListLayout.java',
'tabs/TabsPanel.java',
'tabs/TabsPanelThumbnailView.java',
'Telemetry.java',
'telemetry/core/TelemetryCorePingBuilder.java',
'telemetry/schedulers/TelemetryUploadScheduler.java',
'telemetry/stores/TelemetryPingStore.java',
'telemetry/TelemetryConstants.java',
+ 'telemetry/TelemetryDispatcher.java',
'telemetry/TelemetryPing.java',
'telemetry/TelemetryPingBuilder.java',
'telemetry/TelemetryPingFromStore.java',
'telemetry/TelemetryUploadService.java',
'TelemetryContract.java',
'text/FloatingActionModeCallback.java',
'text/FloatingToolbarTextSelection.java',
'text/TextAction.java',