1 /*
2  * This Source Code Form is subject to the terms of the Mozilla Public
3  * License, v. 2.0. If a copy of the MPL was not distributed with this
4  * file, you can obtain one at http://mozilla.org/MPL/2.0/.
5  */
6 
7 package org.mozilla.gecko.telemetry;
8 
9 import android.content.Context;
10 import android.support.annotation.WorkerThread;
11 import android.util.Log;
12 import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCorePingBuilder;
13 import org.mozilla.gecko.telemetry.pingbuilders.TelemetryCrashPingBuilder;
14 import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadScheduler;
15 import org.mozilla.gecko.telemetry.schedulers.TelemetryUploadAllPingsImmediatelyScheduler;
16 import org.mozilla.gecko.telemetry.stores.TelemetryJSONFilePingStore;
17 import org.mozilla.gecko.telemetry.stores.TelemetryPingStore;
18 import org.mozilla.gecko.util.ThreadUtils;
19 
20 import java.io.File;
21 import java.io.IOException;
22 
23 /**
24  * The entry-point for Java-based telemetry. This class handles:
25  *  * Initializing the Stores & Schedulers.
26  *  * Queueing upload requests for a given ping.
27  *
28  * To test Telemetry , see {@link TelemetryConstants} &
29  * https://wiki.mozilla.org/Mobile/Fennec/Android/Java_telemetry.
30  *
31  * The full architecture is:
32  *
33  * Fennec -(PingBuilder)-> Dispatcher -2-> Scheduler -> UploadService
34  *                             | 1                            |
35  *                           Store <--------------------------
36  *
37  * The store acts as a single store of truth and contains a list of all
38  * pings waiting to be uploaded. The dispatcher will queue a ping to upload
39  * by writing it to the store. Later, the UploadService will try to upload
40  * this queued ping by reading directly from the store.
41  *
42  * To implement a new ping type, you should:
43  *   1) Implement a {@link org.mozilla.gecko.telemetry.pingbuilders.TelemetryPingBuilder} for your ping type.
44  *   2) Re-use a ping store in .../stores/ or implement a new one: {@link TelemetryPingStore}. The
45  * type of store may be affected by robustness requirements (e.g. do you have data in addition to
46  * pings that need to be atomically updated when a ping is stored?) and performance requirements.
47  *   3) Re-use an upload scheduler in .../schedulers/ or implement a new one: {@link TelemetryUploadScheduler}.
48  *   4) Initialize your Store & (if new) Scheduler in the constructor of this class
49  *   5) Add a queuePingForUpload method for your PingBuilder class (see
50  * {@link #queuePingForUpload(Context, TelemetryCorePingBuilder)})
51  *   6) In Fennec, where you want to store a ping and attempt upload, create a PingBuilder and
52  * pass it to the new queuePingForUpload method.
53  */
54 public class TelemetryDispatcher {
55     private static final String LOGTAG = "Gecko" + TelemetryDispatcher.class.getSimpleName();
56 
57     private static final String STORE_CONTAINER_DIR_NAME = "telemetry_java";
58     private static final String CORE_STORE_DIR_NAME = "core";
59     private static final String CRASH_STORE_DIR_NAME = "crash";
60 
61     private final TelemetryJSONFilePingStore coreStore;
62     private final TelemetryJSONFilePingStore crashStore;
63 
64     private final TelemetryUploadAllPingsImmediatelyScheduler uploadAllPingsImmediatelyScheduler;
65 
66     @WorkerThread // via TelemetryJSONFilePingStore
TelemetryDispatcher(final String profilePath, final String profileName)67     public TelemetryDispatcher(final String profilePath, final String profileName) {
68         final String storePath = profilePath + File.separator + STORE_CONTAINER_DIR_NAME;
69 
70         // There are measurements in the core ping (e.g. seq #) that would ideally be atomically updated
71         // when the ping is stored. However, for simplicity, we use the json store and accept the possible
72         // loss of data (see bug 1243585 comment 16+ for more).
73         coreStore = new TelemetryJSONFilePingStore(new File(storePath, CORE_STORE_DIR_NAME), profileName);
74         crashStore = new TelemetryJSONFilePingStore(new File(storePath, CRASH_STORE_DIR_NAME), profileName);
75 
76         uploadAllPingsImmediatelyScheduler = new TelemetryUploadAllPingsImmediatelyScheduler();
77     }
78 
queuePingForUpload(final Context context, final TelemetryOutgoingPing ping, final TelemetryPingStore store, final TelemetryUploadScheduler scheduler)79     private void queuePingForUpload(final Context context, final TelemetryOutgoingPing ping, final TelemetryPingStore store,
80                                     final TelemetryUploadScheduler scheduler) {
81         final QueuePingRunnable runnable = new QueuePingRunnable(context, ping, store, scheduler);
82         ThreadUtils.postToBackgroundThread(runnable); // TODO: Investigate how busy this thread is. See if we want another.
83     }
84 
85     /**
86      * Queues the given ping for upload and potentially schedules upload. This method can be called from any thread.
87      */
queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder)88     public void queuePingForUpload(final Context context, final TelemetryCorePingBuilder pingBuilder) {
89         final TelemetryOutgoingPing ping = pingBuilder.build();
90         queuePingForUpload(context, ping, coreStore, uploadAllPingsImmediatelyScheduler);
91     }
92 
93     /**
94      * Queues the given crash ping for upload and potentially schedules upload. This method can be called from any thread.
95      */
queuePingForUpload(final Context context, final TelemetryCrashPingBuilder pingBuilder)96     public void queuePingForUpload(final Context context, final TelemetryCrashPingBuilder pingBuilder) {
97         final TelemetryOutgoingPing ping = pingBuilder.build();
98         queuePingForUpload(context, ping, crashStore, uploadAllPingsImmediatelyScheduler);
99     }
100 
101     /* package-private */ static class QueuePingRunnable implements Runnable {
102         private final Context applicationContext;
103         private final TelemetryOutgoingPing ping;
104         private final TelemetryPingStore store;
105         private final TelemetryUploadScheduler scheduler;
106 
QueuePingRunnable(final Context context, final TelemetryOutgoingPing ping, final TelemetryPingStore store, final TelemetryUploadScheduler scheduler)107         /* package-private */ QueuePingRunnable(final Context context, final TelemetryOutgoingPing ping, final TelemetryPingStore store,
108                                                 final TelemetryUploadScheduler scheduler) {
109             this.applicationContext = context.getApplicationContext();
110             this.ping = ping;
111             this.store = store;
112             this.scheduler = scheduler;
113         }
114 
115         @Override
run()116         public void run() {
117             // We block while storing the ping so the scheduled upload is guaranteed to have the newly-stored value.
118             try {
119                 store.storePing(ping);
120             } catch (final IOException e) {
121                 // Don't log exception to avoid leaking profile path.
122                 Log.e(LOGTAG, "Unable to write ping to disk. Continuing with upload attempt");
123             }
124 
125             if (scheduler.isReadyToUpload(applicationContext, store)) {
126                 scheduler.scheduleUpload(applicationContext, store);
127             }
128         }
129     }
130 }
131