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