Bug 1243585 - Add TelemetryDispatcher. r=sebastian draft
authorMichael Comella <michael.l.comella@gmail.com>
Thu, 28 Apr 2016 15:36:56 -0700
changeset 357485 17fe62f7a7a08ffa2b58594c65acb62cda972f53
parent 357484 8732a360b28366b840a60d9f7463835f54684093
child 357486 9dbbd325a46492d0de34d7086671c558e9453fe6
push id16806
push usermichael.l.comella@gmail.com
push dateFri, 29 Apr 2016 00:52:31 +0000
reviewerssebastian
bugs1243585
milestone49.0a1
Bug 1243585 - Add TelemetryDispatcher. r=sebastian Note: for version control and review simplicity, this does not yet compile. MozReview-Commit-ID: EvccGtseOKT
mobile/android/base/java/org/mozilla/gecko/telemetry/TelemetryDispatcher.java
mobile/android/base/java/org/mozilla/gecko/telemetry/core/TelemetryCorePingBuilder.java
mobile/android/base/moz.build
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',