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