services/metrics/dataprovider.jsm

Tue, 06 Jan 2015 21:39:09 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Tue, 06 Jan 2015 21:39:09 +0100
branch
TOR_BUG_9701
changeset 8
97036ab72558
permissions
-rw-r--r--

Conditionally force memory storage according to privacy.thirdparty.isolate;
This solves Tor bug #9701, complying with disk avoidance documented in
https://www.torproject.org/projects/torbrowser/design/#disk-avoidance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 #ifndef MERGED_COMPARTMENT
     9 this.EXPORTED_SYMBOLS = [
    10   "Measurement",
    11   "Provider",
    12 ];
    14 const {utils: Cu} = Components;
    16 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
    18 #endif
    20 Cu.import("resource://gre/modules/Promise.jsm");
    21 Cu.import("resource://gre/modules/Preferences.jsm");
    22 Cu.import("resource://gre/modules/Task.jsm");
    23 Cu.import("resource://gre/modules/Log.jsm");
    24 Cu.import("resource://services-common/utils.js");
    28 /**
    29  * Represents a collection of related pieces/fields of data.
    30  *
    31  * This is an abstract base type.
    32  *
    33  * This type provides the primary interface for storing, retrieving, and
    34  * serializing data.
    35  *
    36  * Each measurement consists of a set of named fields. Each field is primarily
    37  * identified by a string name, which must be unique within the measurement.
    38  *
    39  * Each derived type must define the following properties:
    40  *
    41  *   name -- String name of this measurement. This is the primary way
    42  *     measurements are distinguished within a provider.
    43  *
    44  *   version -- Integer version of this measurement. This is a secondary
    45  *     identifier for a measurement within a provider. The version denotes
    46  *     the behavior of this measurement and the composition of its fields over
    47  *     time. When a new field is added or the behavior of an existing field
    48  *     changes, the version should be incremented. The initial version of a
    49  *     measurement is typically 1.
    50  *
    51  *   fields -- Object defining the fields this measurement holds. Keys in the
    52  *     object are string field names. Values are objects describing how the
    53  *     field works. The following properties are recognized:
    54  *
    55  *       type -- The string type of this field. This is typically one of the
    56  *         FIELD_* constants from the Metrics.Storage type.
    57  *
    58  *
    59  * FUTURE: provide hook points for measurements to supplement with custom
    60  * storage needs.
    61  */
    62 this.Measurement = function () {
    63   if (!this.name) {
    64     throw new Error("Measurement must have a name.");
    65   }
    67   if (!this.version) {
    68     throw new Error("Measurement must have a version.");
    69   }
    71   if (!Number.isInteger(this.version)) {
    72     throw new Error("Measurement's version must be an integer: " + this.version);
    73   }
    75   if (!this.fields) {
    76     throw new Error("Measurement must define fields.");
    77   }
    79   for (let [name, info] in Iterator(this.fields)) {
    80     if (!info) {
    81       throw new Error("Field does not contain metadata: " + name);
    82     }
    84     if (!info.type) {
    85       throw new Error("Field is missing required type property: " + name);
    86     }
    87   }
    89   this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name);
    91   this.id = null;
    92   this.storage = null;
    93   this._fields = {};
    95   this._serializers = {};
    96   this._serializers[this.SERIALIZE_JSON] = {
    97     singular: this._serializeJSONSingular.bind(this),
    98     daily: this._serializeJSONDay.bind(this),
    99   };
   100 }
   102 Measurement.prototype = Object.freeze({
   103   SERIALIZE_JSON: "json",
   105   /**
   106    * Obtain a serializer for this measurement.
   107    *
   108    * Implementations should return an object with the following keys:
   109    *
   110    *   singular -- Serializer for singular data.
   111    *   daily -- Serializer for daily data.
   112    *
   113    * Each item is a function that takes a single argument: the data to
   114    * serialize. The passed data is a subset of that returned from
   115    * this.getValues(). For "singular," data.singular is passed. For "daily",
   116    * data.days.get(<day>) is passed.
   117    *
   118    * This function receives a single argument: the serialization format we
   119    * are requesting. This is one of the SERIALIZE_* constants on this base type.
   120    *
   121    * For SERIALIZE_JSON, the function should return an object that
   122    * JSON.stringify() knows how to handle. This could be an anonymous object or
   123    * array or any object with a property named `toJSON` whose value is a
   124    * function. The returned object will be added to a larger document
   125    * containing the results of all `serialize` calls.
   126    *
   127    * The default implementation knows how to serialize built-in types using
   128    * very simple logic. If small encoding size is a goal, the default
   129    * implementation may not be suitable. If an unknown field type is
   130    * encountered, the default implementation will error.
   131    *
   132    * @param format
   133    *        (string) A SERIALIZE_* constant defining what serialization format
   134    *        to use.
   135    */
   136   serializer: function (format) {
   137     if (!(format in this._serializers)) {
   138       throw new Error("Don't know how to serialize format: " + format);
   139     }
   141     return this._serializers[format];
   142   },
   144   /**
   145    * Whether this measurement contains the named field.
   146    *
   147    * @param name
   148    *        (string) Name of field.
   149    *
   150    * @return bool
   151    */
   152   hasField: function (name) {
   153     return name in this.fields;
   154   },
   156   /**
   157    * The unique identifier for a named field.
   158    *
   159    * This will throw if the field is not known.
   160    *
   161    * @param name
   162    *        (string) Name of field.
   163    */
   164   fieldID: function (name) {
   165     let entry = this._fields[name];
   167     if (!entry) {
   168       throw new Error("Unknown field: " + name);
   169     }
   171     return entry[0];
   172   },
   174   fieldType: function (name) {
   175     let entry = this._fields[name];
   177     if (!entry) {
   178       throw new Error("Unknown field: " + name);
   179     }
   181     return entry[1];
   182   },
   184   _configureStorage: function () {
   185     let missing = [];
   186     for (let [name, info] in Iterator(this.fields)) {
   187       if (this.storage.hasFieldFromMeasurement(this.id, name)) {
   188         this._fields[name] =
   189           [this.storage.fieldIDFromMeasurement(this.id, name), info.type];
   190         continue;
   191       }
   193       missing.push([name, info.type]);
   194     }
   196     if (!missing.length) {
   197       return CommonUtils.laterTickResolvingPromise();
   198     }
   200     // We only perform a transaction if we have work to do (to avoid
   201     // extra SQLite overhead).
   202     return this.storage.enqueueTransaction(function registerFields() {
   203       for (let [name, type] of missing) {
   204         this._log.debug("Registering field: " + name + " " + type);
   205         let id = yield this.storage.registerField(this.id, name, type);
   206         this._fields[name] = [id, type];
   207       }
   208     }.bind(this));
   209   },
   211   //---------------------------------------------------------------------------
   212   // Data Recording Functions
   213   //
   214   // Functions in this section are used to record new values against this
   215   // measurement instance.
   216   //
   217   // Generally speaking, these functions will throw if the specified field does
   218   // not exist or if the storage function requested is not appropriate for the
   219   // type of that field. These functions will also return a promise that will
   220   // be resolved when the underlying storage operation has completed.
   221   //---------------------------------------------------------------------------
   223   /**
   224    * Increment a daily counter field in this measurement by 1.
   225    *
   226    * By default, the counter for the current day will be incremented.
   227    *
   228    * If the field is not known or is not a daily counter, this will throw.
   229    *
   230    *
   231    *
   232    * @param field
   233    *        (string) The name of the field whose value to increment.
   234    * @param date
   235    *        (Date) Day on which to increment the counter.
   236    * @param by
   237    *        (integer) How much to increment by.
   238    * @return Promise<>
   239    */
   240   incrementDailyCounter: function (field, date=new Date(), by=1) {
   241     return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
   242                                                          date, by);
   243   },
   245   /**
   246    * Record a new numeric value for a daily discrete numeric field.
   247    *
   248    * @param field
   249    *        (string) The name of the field to append a value to.
   250    * @param value
   251    *        (Number) Number to append.
   252    * @param date
   253    *        (Date) Day on which to append the value.
   254    *
   255    * @return Promise<>
   256    */
   257   addDailyDiscreteNumeric: function (field, value, date=new Date()) {
   258     return this.storage.addDailyDiscreteNumericFromFieldID(
   259                           this.fieldID(field), value, date);
   260   },
   262   /**
   263    * Record a new text value for a daily discrete text field.
   264    *
   265    * This is like `addDailyDiscreteNumeric` but for daily discrete text fields.
   266    */
   267   addDailyDiscreteText: function (field, value, date=new Date()) {
   268     return this.storage.addDailyDiscreteTextFromFieldID(
   269                           this.fieldID(field), value, date);
   270   },
   272   /**
   273    * Record the last seen value for a last numeric field.
   274    *
   275    * @param field
   276    *        (string) The name of the field to set the value of.
   277    * @param value
   278    *        (Number) The value to set.
   279    * @param date
   280    *        (Date) When this value was recorded.
   281    *
   282    * @return Promise<>
   283    */
   284   setLastNumeric: function (field, value, date=new Date()) {
   285     return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
   286                                                   date);
   287   },
   289   /**
   290    * Record the last seen value for a last text field.
   291    *
   292    * This is like `setLastNumeric` except for last text fields.
   293    */
   294   setLastText: function (field, value, date=new Date()) {
   295     return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
   296                                                date);
   297   },
   299   /**
   300    * Record the most recent value for a daily last numeric field.
   301    *
   302    * @param field
   303    *        (string) The name of a daily last numeric field.
   304    * @param value
   305    *        (Number) The value to set.
   306    * @param date
   307    *        (Date) Day on which to record the last value.
   308    *
   309    * @return Promise<>
   310    */
   311   setDailyLastNumeric: function (field, value, date=new Date()) {
   312     return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
   313                                                        value, date);
   314   },
   316   /**
   317    * Record the most recent value for a daily last text field.
   318    *
   319    * This is like `setDailyLastNumeric` except for a daily last text field.
   320    */
   321   setDailyLastText: function (field, value, date=new Date()) {
   322     return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
   323                                                     value, date);
   324   },
   326   //---------------------------------------------------------------------------
   327   // End of data recording APIs.
   328   //---------------------------------------------------------------------------
   330   /**
   331    * Obtain all values stored for this measurement.
   332    *
   333    * The default implementation obtains all known types from storage. If the
   334    * measurement provides custom types or stores values somewhere other than
   335    * storage, it should define its own implementation.
   336    *
   337    * This returns a promise that resolves to a data structure which is
   338    * understood by the measurement's serialize() function.
   339    */
   340   getValues: function () {
   341     return this.storage.getMeasurementValues(this.id);
   342   },
   344   deleteLastNumeric: function (field) {
   345     return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
   346   },
   348   deleteLastText: function (field) {
   349     return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
   350   },
   352   /**
   353    * This method is used by the default serializers to control whether a field
   354    * is included in the output.
   355    *
   356    * There could be legacy fields in storage we no longer care about.
   357    *
   358    * This method is a hook to allow measurements to change this behavior, e.g.,
   359    * to implement a dynamic fieldset.
   360    *
   361    * You will also need to override `fieldType`.
   362    *
   363    * @return (boolean) true if the specified field should be included in
   364    *                   payload output.
   365    */
   366   shouldIncludeField: function (field) {
   367     return field in this._fields;
   368   },
   370   _serializeJSONSingular: function (data) {
   371     let result = {"_v": this.version};
   373     for (let [field, data] of data) {
   374       // There could be legacy fields in storage we no longer care about.
   375       if (!this.shouldIncludeField(field)) {
   376         continue;
   377       }
   379       let type = this.fieldType(field);
   381       switch (type) {
   382         case this.storage.FIELD_LAST_NUMERIC:
   383         case this.storage.FIELD_LAST_TEXT:
   384           result[field] = data[1];
   385           break;
   387         case this.storage.FIELD_DAILY_COUNTER:
   388         case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
   389         case this.storage.FIELD_DAILY_DISCRETE_TEXT:
   390         case this.storage.FIELD_DAILY_LAST_NUMERIC:
   391         case this.storage.FIELD_DAILY_LAST_TEXT:
   392           continue;
   394         default:
   395           throw new Error("Unknown field type: " + type);
   396       }
   397     }
   399     return result;
   400   },
   402   _serializeJSONDay: function (data) {
   403     let result = {"_v": this.version};
   405     for (let [field, data] of data) {
   406       if (!this.shouldIncludeField(field)) {
   407         continue;
   408       }
   410       let type = this.fieldType(field);
   412       switch (type) {
   413         case this.storage.FIELD_DAILY_COUNTER:
   414         case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
   415         case this.storage.FIELD_DAILY_DISCRETE_TEXT:
   416         case this.storage.FIELD_DAILY_LAST_NUMERIC:
   417         case this.storage.FIELD_DAILY_LAST_TEXT:
   418           result[field] = data;
   419           break;
   421         case this.storage.FIELD_LAST_NUMERIC:
   422         case this.storage.FIELD_LAST_TEXT:
   423           continue;
   425         default:
   426           throw new Error("Unknown field type: " + type);
   427       }
   428     }
   430     return result;
   431   },
   432 });
   435 /**
   436  * An entity that emits data.
   437  *
   438  * A `Provider` consists of a string name (must be globally unique among all
   439  * known providers) and a set of `Measurement` instances.
   440  *
   441  * The main role of a `Provider` is to produce metrics data and to store said
   442  * data in the storage backend.
   443  *
   444  * Metrics data collection is initiated either by a manager calling a
   445  * `collect*` function on `Provider` instances or by the `Provider` registering
   446  * to some external event and then reacting whenever they occur.
   447  *
   448  * `Provider` implementations interface directly with a storage backend. For
   449  * common stored values (daily counters, daily discrete values, etc),
   450  * implementations should interface with storage via the various helper
   451  * functions on the `Measurement` instances. For custom stored value types,
   452  * implementations will interact directly with the low-level storage APIs.
   453  *
   454  * Because multiple providers exist and could be responding to separate
   455  * external events simultaneously and because not all operations performed by
   456  * storage can safely be performed in parallel, writing directly to storage at
   457  * event time is dangerous. Therefore, interactions with storage must be
   458  * deferred until it is safe to perform them.
   459  *
   460  * This typically looks something like:
   461  *
   462  *   // This gets called when an external event worthy of recording metrics
   463  *   // occurs. The function receives a numeric value associated with the event.
   464  *   function onExternalEvent (value) {
   465  *     let now = new Date();
   466  *     let m = this.getMeasurement("foo", 1);
   467  *
   468  *     this.enqueueStorageOperation(function storeExternalEvent() {
   469  *
   470  *       // We interface with storage via the `Measurement` helper functions.
   471  *       // These each return a promise that will be resolved when the
   472  *       // operation finishes. We rely on behavior of storage where operations
   473  *       // are executed single threaded and sequentially. Therefore, we only
   474  *       // need to return the final promise.
   475  *       m.incrementDailyCounter("foo", now);
   476  *       return m.addDailyDiscreteNumericValue("my_value", value, now);
   477  *     }.bind(this));
   478  *
   479  *   }
   480  *
   481  *
   482  * `Provider` is an abstract base class. Implementations must define a few
   483  * properties:
   484  *
   485  *   name
   486  *     The `name` property should be a string defining the provider's name. The
   487  *     name must be globally unique for the application. The name is used as an
   488  *     identifier to distinguish providers from each other.
   489  *
   490  *   measurementTypes
   491  *     This must be an array of `Measurement`-derived types. Note that elements
   492  *     in the array are the type functions, not instances. Instances of the
   493  *     `Measurement` are created at run-time by the `Provider` and are bound
   494  *     to the provider and to a specific storage backend.
   495  */
   496 this.Provider = function () {
   497   if (!this.name) {
   498     throw new Error("Provider must define a name.");
   499   }
   501   if (!Array.isArray(this.measurementTypes)) {
   502     throw new Error("Provider must define measurement types.");
   503   }
   505   this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name);
   507   this.measurements = null;
   508   this.storage = null;
   509 }
   511 Provider.prototype = Object.freeze({
   512   /**
   513    * Whether the provider only pulls data from other sources.
   514    *
   515    * If this is true, the provider pulls data from other sources. By contrast,
   516    * "push-based" providers subscribe to foreign sources and record/react to
   517    * external events as they happen.
   518    *
   519    * Pull-only providers likely aren't instantiated until a data collection
   520    * is performed. Thus, implementations cannot rely on a provider instance
   521    * always being alive. This is an optimization so provider instances aren't
   522    * dead weight while the application is running.
   523    *
   524    * This must be set on the prototype to have an effect.
   525    */
   526   pullOnly: false,
   528   /**
   529    * Obtain a `Measurement` from its name and version.
   530    *
   531    * If the measurement is not found, an Error is thrown.
   532    */
   533   getMeasurement: function (name, version) {
   534     if (!Number.isInteger(version)) {
   535       throw new Error("getMeasurement expects an integer version. Got: " + version);
   536     }
   538     let m = this.measurements.get([name, version].join(":"));
   540     if (!m) {
   541       throw new Error("Unknown measurement: " + name + " v" + version);
   542     }
   544     return m;
   545   },
   547   init: function (storage) {
   548     if (this.storage !== null) {
   549       throw new Error("Provider() not called. Did the sub-type forget to call it?");
   550     }
   552     if (this.storage) {
   553       throw new Error("Provider has already been initialized.");
   554     }
   556     this.measurements = new Map();
   557     this.storage = storage;
   559     let self = this;
   560     return Task.spawn(function init() {
   561       let pre = self.preInit();
   562       if (!pre || typeof(pre.then) != "function") {
   563         throw new Error("preInit() does not return a promise.");
   564       }
   565       yield pre;
   567       for (let measurementType of self.measurementTypes) {
   568         let measurement = new measurementType();
   570         measurement.provider = self;
   571         measurement.storage = self.storage;
   573         let id = yield storage.registerMeasurement(self.name, measurement.name,
   574                                                    measurement.version);
   576         measurement.id = id;
   578         yield measurement._configureStorage();
   580         self.measurements.set([measurement.name, measurement.version].join(":"),
   581                               measurement);
   582       }
   584       let post = self.postInit();
   585       if (!post || typeof(post.then) != "function") {
   586         throw new Error("postInit() does not return a promise.");
   587       }
   588       yield post;
   589     });
   590   },
   592   shutdown: function () {
   593     let promise = this.onShutdown();
   595     if (!promise || typeof(promise.then) != "function") {
   596       throw new Error("onShutdown implementation does not return a promise.");
   597     }
   599     return promise;
   600   },
   602   /**
   603    * Hook point for implementations to perform pre-initialization activity.
   604    *
   605    * This method will be called before measurement registration.
   606    *
   607    * Implementations should return a promise which is resolved when
   608    * initialization activities have completed.
   609    */
   610   preInit: function () {
   611     return CommonUtils.laterTickResolvingPromise();
   612   },
   614   /**
   615    * Hook point for implementations to perform post-initialization activity.
   616    *
   617    * This method will be called after `preInit` and measurement registration,
   618    * but before initialization is finished.
   619    *
   620    * If a `Provider` instance needs to register observers, etc, it should
   621    * implement this function.
   622    *
   623    * Implementations should return a promise which is resolved when
   624    * initialization activities have completed.
   625    */
   626   postInit: function () {
   627     return CommonUtils.laterTickResolvingPromise();
   628   },
   630   /**
   631    * Hook point for shutdown of instances.
   632    *
   633    * This is the opposite of `onInit`. If a `Provider` needs to unregister
   634    * observers, etc, this is where it should do it.
   635    *
   636    * Implementations should return a promise which is resolved when
   637    * shutdown activities have completed.
   638    */
   639   onShutdown: function () {
   640     return CommonUtils.laterTickResolvingPromise();
   641   },
   643   /**
   644    * Collects data that doesn't change during the application's lifetime.
   645    *
   646    * Implementations should return a promise that resolves when all data has
   647    * been collected and storage operations have been finished.
   648    *
   649    * @return Promise<>
   650    */
   651   collectConstantData: function () {
   652     return CommonUtils.laterTickResolvingPromise();
   653   },
   655   /**
   656    * Collects data approximately every day.
   657    *
   658    * For long-running applications, this is called approximately every day.
   659    * It may or may not be called every time the application is run. It also may
   660    * be called more than once per day.
   661    *
   662    * Implementations should return a promise that resolves when all data has
   663    * been collected and storage operations have completed.
   664    *
   665    * @return Promise<>
   666    */
   667   collectDailyData: function () {
   668     return CommonUtils.laterTickResolvingPromise();
   669   },
   671   /**
   672    * Queue a deferred storage operation.
   673    *
   674    * Deferred storage operations are the preferred method for providers to
   675    * interact with storage. When collected data is to be added to storage,
   676    * the provider creates a function that performs the necessary storage
   677    * interactions and then passes that function to this function. Pending
   678    * storage operations will be executed sequentially by a coordinator.
   679    *
   680    * The passed function should return a promise which will be resolved upon
   681    * completion of storage interaction.
   682    */
   683   enqueueStorageOperation: function (func) {
   684     return this.storage.enqueueOperation(func);
   685   },
   687   /**
   688    * Obtain persisted provider state.
   689    *
   690    * Provider state consists of key-value pairs of string names and values.
   691    * Providers can stuff whatever they want into state. They are encouraged to
   692    * store as little as possible for performance reasons.
   693    *
   694    * State is backed by storage and is robust.
   695    *
   696    * These functions do not enqueue on storage automatically, so they should
   697    * be guarded by `enqueueStorageOperation` or some other mutex.
   698    *
   699    * @param key
   700    *        (string) The property to retrieve.
   701    *
   702    * @return Promise<string|null> String value on success. null if no state
   703    *         is available under this key.
   704    */
   705   getState: function (key) {
   706     return this.storage.getProviderState(this.name, key);
   707   },
   709   /**
   710    * Set state for this provider.
   711    *
   712    * This is the complementary API for `getState` and obeys the same
   713    * storage restrictions.
   714    */
   715   setState: function (key, value) {
   716     return this.storage.setProviderState(this.name, key, value);
   717   },
   719   _dateToDays: function (date) {
   720     return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
   721   },
   723   _daysToDate: function (days) {
   724     return new Date(days * MILLISECONDS_PER_DAY);
   725   },
   726 });

mercurial