diff -r 000000000000 -r 6474c204b198 services/metrics/dataprovider.jsm --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/services/metrics/dataprovider.jsm Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,727 @@ +/* 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/. */ + +"use strict"; + +#ifndef MERGED_COMPARTMENT + +this.EXPORTED_SYMBOLS = [ + "Measurement", + "Provider", +]; + +const {utils: Cu} = Components; + +const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000; + +#endif + +Cu.import("resource://gre/modules/Promise.jsm"); +Cu.import("resource://gre/modules/Preferences.jsm"); +Cu.import("resource://gre/modules/Task.jsm"); +Cu.import("resource://gre/modules/Log.jsm"); +Cu.import("resource://services-common/utils.js"); + + + +/** + * Represents a collection of related pieces/fields of data. + * + * This is an abstract base type. + * + * This type provides the primary interface for storing, retrieving, and + * serializing data. + * + * Each measurement consists of a set of named fields. Each field is primarily + * identified by a string name, which must be unique within the measurement. + * + * Each derived type must define the following properties: + * + * name -- String name of this measurement. This is the primary way + * measurements are distinguished within a provider. + * + * version -- Integer version of this measurement. This is a secondary + * identifier for a measurement within a provider. The version denotes + * the behavior of this measurement and the composition of its fields over + * time. When a new field is added or the behavior of an existing field + * changes, the version should be incremented. The initial version of a + * measurement is typically 1. + * + * fields -- Object defining the fields this measurement holds. Keys in the + * object are string field names. Values are objects describing how the + * field works. The following properties are recognized: + * + * type -- The string type of this field. This is typically one of the + * FIELD_* constants from the Metrics.Storage type. + * + * + * FUTURE: provide hook points for measurements to supplement with custom + * storage needs. + */ +this.Measurement = function () { + if (!this.name) { + throw new Error("Measurement must have a name."); + } + + if (!this.version) { + throw new Error("Measurement must have a version."); + } + + if (!Number.isInteger(this.version)) { + throw new Error("Measurement's version must be an integer: " + this.version); + } + + if (!this.fields) { + throw new Error("Measurement must define fields."); + } + + for (let [name, info] in Iterator(this.fields)) { + if (!info) { + throw new Error("Field does not contain metadata: " + name); + } + + if (!info.type) { + throw new Error("Field is missing required type property: " + name); + } + } + + this._log = Log.repository.getLogger("Services.Metrics.Measurement." + this.name); + + this.id = null; + this.storage = null; + this._fields = {}; + + this._serializers = {}; + this._serializers[this.SERIALIZE_JSON] = { + singular: this._serializeJSONSingular.bind(this), + daily: this._serializeJSONDay.bind(this), + }; +} + +Measurement.prototype = Object.freeze({ + SERIALIZE_JSON: "json", + + /** + * Obtain a serializer for this measurement. + * + * Implementations should return an object with the following keys: + * + * singular -- Serializer for singular data. + * daily -- Serializer for daily data. + * + * Each item is a function that takes a single argument: the data to + * serialize. The passed data is a subset of that returned from + * this.getValues(). For "singular," data.singular is passed. For "daily", + * data.days.get() is passed. + * + * This function receives a single argument: the serialization format we + * are requesting. This is one of the SERIALIZE_* constants on this base type. + * + * For SERIALIZE_JSON, the function should return an object that + * JSON.stringify() knows how to handle. This could be an anonymous object or + * array or any object with a property named `toJSON` whose value is a + * function. The returned object will be added to a larger document + * containing the results of all `serialize` calls. + * + * The default implementation knows how to serialize built-in types using + * very simple logic. If small encoding size is a goal, the default + * implementation may not be suitable. If an unknown field type is + * encountered, the default implementation will error. + * + * @param format + * (string) A SERIALIZE_* constant defining what serialization format + * to use. + */ + serializer: function (format) { + if (!(format in this._serializers)) { + throw new Error("Don't know how to serialize format: " + format); + } + + return this._serializers[format]; + }, + + /** + * Whether this measurement contains the named field. + * + * @param name + * (string) Name of field. + * + * @return bool + */ + hasField: function (name) { + return name in this.fields; + }, + + /** + * The unique identifier for a named field. + * + * This will throw if the field is not known. + * + * @param name + * (string) Name of field. + */ + fieldID: function (name) { + let entry = this._fields[name]; + + if (!entry) { + throw new Error("Unknown field: " + name); + } + + return entry[0]; + }, + + fieldType: function (name) { + let entry = this._fields[name]; + + if (!entry) { + throw new Error("Unknown field: " + name); + } + + return entry[1]; + }, + + _configureStorage: function () { + let missing = []; + for (let [name, info] in Iterator(this.fields)) { + if (this.storage.hasFieldFromMeasurement(this.id, name)) { + this._fields[name] = + [this.storage.fieldIDFromMeasurement(this.id, name), info.type]; + continue; + } + + missing.push([name, info.type]); + } + + if (!missing.length) { + return CommonUtils.laterTickResolvingPromise(); + } + + // We only perform a transaction if we have work to do (to avoid + // extra SQLite overhead). + return this.storage.enqueueTransaction(function registerFields() { + for (let [name, type] of missing) { + this._log.debug("Registering field: " + name + " " + type); + let id = yield this.storage.registerField(this.id, name, type); + this._fields[name] = [id, type]; + } + }.bind(this)); + }, + + //--------------------------------------------------------------------------- + // Data Recording Functions + // + // Functions in this section are used to record new values against this + // measurement instance. + // + // Generally speaking, these functions will throw if the specified field does + // not exist or if the storage function requested is not appropriate for the + // type of that field. These functions will also return a promise that will + // be resolved when the underlying storage operation has completed. + //--------------------------------------------------------------------------- + + /** + * Increment a daily counter field in this measurement by 1. + * + * By default, the counter for the current day will be incremented. + * + * If the field is not known or is not a daily counter, this will throw. + * + * + * + * @param field + * (string) The name of the field whose value to increment. + * @param date + * (Date) Day on which to increment the counter. + * @param by + * (integer) How much to increment by. + * @return Promise<> + */ + incrementDailyCounter: function (field, date=new Date(), by=1) { + return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field), + date, by); + }, + + /** + * Record a new numeric value for a daily discrete numeric field. + * + * @param field + * (string) The name of the field to append a value to. + * @param value + * (Number) Number to append. + * @param date + * (Date) Day on which to append the value. + * + * @return Promise<> + */ + addDailyDiscreteNumeric: function (field, value, date=new Date()) { + return this.storage.addDailyDiscreteNumericFromFieldID( + this.fieldID(field), value, date); + }, + + /** + * Record a new text value for a daily discrete text field. + * + * This is like `addDailyDiscreteNumeric` but for daily discrete text fields. + */ + addDailyDiscreteText: function (field, value, date=new Date()) { + return this.storage.addDailyDiscreteTextFromFieldID( + this.fieldID(field), value, date); + }, + + /** + * Record the last seen value for a last numeric field. + * + * @param field + * (string) The name of the field to set the value of. + * @param value + * (Number) The value to set. + * @param date + * (Date) When this value was recorded. + * + * @return Promise<> + */ + setLastNumeric: function (field, value, date=new Date()) { + return this.storage.setLastNumericFromFieldID(this.fieldID(field), value, + date); + }, + + /** + * Record the last seen value for a last text field. + * + * This is like `setLastNumeric` except for last text fields. + */ + setLastText: function (field, value, date=new Date()) { + return this.storage.setLastTextFromFieldID(this.fieldID(field), value, + date); + }, + + /** + * Record the most recent value for a daily last numeric field. + * + * @param field + * (string) The name of a daily last numeric field. + * @param value + * (Number) The value to set. + * @param date + * (Date) Day on which to record the last value. + * + * @return Promise<> + */ + setDailyLastNumeric: function (field, value, date=new Date()) { + return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field), + value, date); + }, + + /** + * Record the most recent value for a daily last text field. + * + * This is like `setDailyLastNumeric` except for a daily last text field. + */ + setDailyLastText: function (field, value, date=new Date()) { + return this.storage.setDailyLastTextFromFieldID(this.fieldID(field), + value, date); + }, + + //--------------------------------------------------------------------------- + // End of data recording APIs. + //--------------------------------------------------------------------------- + + /** + * Obtain all values stored for this measurement. + * + * The default implementation obtains all known types from storage. If the + * measurement provides custom types or stores values somewhere other than + * storage, it should define its own implementation. + * + * This returns a promise that resolves to a data structure which is + * understood by the measurement's serialize() function. + */ + getValues: function () { + return this.storage.getMeasurementValues(this.id); + }, + + deleteLastNumeric: function (field) { + return this.storage.deleteLastNumericFromFieldID(this.fieldID(field)); + }, + + deleteLastText: function (field) { + return this.storage.deleteLastTextFromFieldID(this.fieldID(field)); + }, + + /** + * This method is used by the default serializers to control whether a field + * is included in the output. + * + * There could be legacy fields in storage we no longer care about. + * + * This method is a hook to allow measurements to change this behavior, e.g., + * to implement a dynamic fieldset. + * + * You will also need to override `fieldType`. + * + * @return (boolean) true if the specified field should be included in + * payload output. + */ + shouldIncludeField: function (field) { + return field in this._fields; + }, + + _serializeJSONSingular: function (data) { + let result = {"_v": this.version}; + + for (let [field, data] of data) { + // There could be legacy fields in storage we no longer care about. + if (!this.shouldIncludeField(field)) { + continue; + } + + let type = this.fieldType(field); + + switch (type) { + case this.storage.FIELD_LAST_NUMERIC: + case this.storage.FIELD_LAST_TEXT: + result[field] = data[1]; + break; + + case this.storage.FIELD_DAILY_COUNTER: + case this.storage.FIELD_DAILY_DISCRETE_NUMERIC: + case this.storage.FIELD_DAILY_DISCRETE_TEXT: + case this.storage.FIELD_DAILY_LAST_NUMERIC: + case this.storage.FIELD_DAILY_LAST_TEXT: + continue; + + default: + throw new Error("Unknown field type: " + type); + } + } + + return result; + }, + + _serializeJSONDay: function (data) { + let result = {"_v": this.version}; + + for (let [field, data] of data) { + if (!this.shouldIncludeField(field)) { + continue; + } + + let type = this.fieldType(field); + + switch (type) { + case this.storage.FIELD_DAILY_COUNTER: + case this.storage.FIELD_DAILY_DISCRETE_NUMERIC: + case this.storage.FIELD_DAILY_DISCRETE_TEXT: + case this.storage.FIELD_DAILY_LAST_NUMERIC: + case this.storage.FIELD_DAILY_LAST_TEXT: + result[field] = data; + break; + + case this.storage.FIELD_LAST_NUMERIC: + case this.storage.FIELD_LAST_TEXT: + continue; + + default: + throw new Error("Unknown field type: " + type); + } + } + + return result; + }, +}); + + +/** + * An entity that emits data. + * + * A `Provider` consists of a string name (must be globally unique among all + * known providers) and a set of `Measurement` instances. + * + * The main role of a `Provider` is to produce metrics data and to store said + * data in the storage backend. + * + * Metrics data collection is initiated either by a manager calling a + * `collect*` function on `Provider` instances or by the `Provider` registering + * to some external event and then reacting whenever they occur. + * + * `Provider` implementations interface directly with a storage backend. For + * common stored values (daily counters, daily discrete values, etc), + * implementations should interface with storage via the various helper + * functions on the `Measurement` instances. For custom stored value types, + * implementations will interact directly with the low-level storage APIs. + * + * Because multiple providers exist and could be responding to separate + * external events simultaneously and because not all operations performed by + * storage can safely be performed in parallel, writing directly to storage at + * event time is dangerous. Therefore, interactions with storage must be + * deferred until it is safe to perform them. + * + * This typically looks something like: + * + * // This gets called when an external event worthy of recording metrics + * // occurs. The function receives a numeric value associated with the event. + * function onExternalEvent (value) { + * let now = new Date(); + * let m = this.getMeasurement("foo", 1); + * + * this.enqueueStorageOperation(function storeExternalEvent() { + * + * // We interface with storage via the `Measurement` helper functions. + * // These each return a promise that will be resolved when the + * // operation finishes. We rely on behavior of storage where operations + * // are executed single threaded and sequentially. Therefore, we only + * // need to return the final promise. + * m.incrementDailyCounter("foo", now); + * return m.addDailyDiscreteNumericValue("my_value", value, now); + * }.bind(this)); + * + * } + * + * + * `Provider` is an abstract base class. Implementations must define a few + * properties: + * + * name + * The `name` property should be a string defining the provider's name. The + * name must be globally unique for the application. The name is used as an + * identifier to distinguish providers from each other. + * + * measurementTypes + * This must be an array of `Measurement`-derived types. Note that elements + * in the array are the type functions, not instances. Instances of the + * `Measurement` are created at run-time by the `Provider` and are bound + * to the provider and to a specific storage backend. + */ +this.Provider = function () { + if (!this.name) { + throw new Error("Provider must define a name."); + } + + if (!Array.isArray(this.measurementTypes)) { + throw new Error("Provider must define measurement types."); + } + + this._log = Log.repository.getLogger("Services.Metrics.Provider." + this.name); + + this.measurements = null; + this.storage = null; +} + +Provider.prototype = Object.freeze({ + /** + * Whether the provider only pulls data from other sources. + * + * If this is true, the provider pulls data from other sources. By contrast, + * "push-based" providers subscribe to foreign sources and record/react to + * external events as they happen. + * + * Pull-only providers likely aren't instantiated until a data collection + * is performed. Thus, implementations cannot rely on a provider instance + * always being alive. This is an optimization so provider instances aren't + * dead weight while the application is running. + * + * This must be set on the prototype to have an effect. + */ + pullOnly: false, + + /** + * Obtain a `Measurement` from its name and version. + * + * If the measurement is not found, an Error is thrown. + */ + getMeasurement: function (name, version) { + if (!Number.isInteger(version)) { + throw new Error("getMeasurement expects an integer version. Got: " + version); + } + + let m = this.measurements.get([name, version].join(":")); + + if (!m) { + throw new Error("Unknown measurement: " + name + " v" + version); + } + + return m; + }, + + init: function (storage) { + if (this.storage !== null) { + throw new Error("Provider() not called. Did the sub-type forget to call it?"); + } + + if (this.storage) { + throw new Error("Provider has already been initialized."); + } + + this.measurements = new Map(); + this.storage = storage; + + let self = this; + return Task.spawn(function init() { + let pre = self.preInit(); + if (!pre || typeof(pre.then) != "function") { + throw new Error("preInit() does not return a promise."); + } + yield pre; + + for (let measurementType of self.measurementTypes) { + let measurement = new measurementType(); + + measurement.provider = self; + measurement.storage = self.storage; + + let id = yield storage.registerMeasurement(self.name, measurement.name, + measurement.version); + + measurement.id = id; + + yield measurement._configureStorage(); + + self.measurements.set([measurement.name, measurement.version].join(":"), + measurement); + } + + let post = self.postInit(); + if (!post || typeof(post.then) != "function") { + throw new Error("postInit() does not return a promise."); + } + yield post; + }); + }, + + shutdown: function () { + let promise = this.onShutdown(); + + if (!promise || typeof(promise.then) != "function") { + throw new Error("onShutdown implementation does not return a promise."); + } + + return promise; + }, + + /** + * Hook point for implementations to perform pre-initialization activity. + * + * This method will be called before measurement registration. + * + * Implementations should return a promise which is resolved when + * initialization activities have completed. + */ + preInit: function () { + return CommonUtils.laterTickResolvingPromise(); + }, + + /** + * Hook point for implementations to perform post-initialization activity. + * + * This method will be called after `preInit` and measurement registration, + * but before initialization is finished. + * + * If a `Provider` instance needs to register observers, etc, it should + * implement this function. + * + * Implementations should return a promise which is resolved when + * initialization activities have completed. + */ + postInit: function () { + return CommonUtils.laterTickResolvingPromise(); + }, + + /** + * Hook point for shutdown of instances. + * + * This is the opposite of `onInit`. If a `Provider` needs to unregister + * observers, etc, this is where it should do it. + * + * Implementations should return a promise which is resolved when + * shutdown activities have completed. + */ + onShutdown: function () { + return CommonUtils.laterTickResolvingPromise(); + }, + + /** + * Collects data that doesn't change during the application's lifetime. + * + * Implementations should return a promise that resolves when all data has + * been collected and storage operations have been finished. + * + * @return Promise<> + */ + collectConstantData: function () { + return CommonUtils.laterTickResolvingPromise(); + }, + + /** + * Collects data approximately every day. + * + * For long-running applications, this is called approximately every day. + * It may or may not be called every time the application is run. It also may + * be called more than once per day. + * + * Implementations should return a promise that resolves when all data has + * been collected and storage operations have completed. + * + * @return Promise<> + */ + collectDailyData: function () { + return CommonUtils.laterTickResolvingPromise(); + }, + + /** + * Queue a deferred storage operation. + * + * Deferred storage operations are the preferred method for providers to + * interact with storage. When collected data is to be added to storage, + * the provider creates a function that performs the necessary storage + * interactions and then passes that function to this function. Pending + * storage operations will be executed sequentially by a coordinator. + * + * The passed function should return a promise which will be resolved upon + * completion of storage interaction. + */ + enqueueStorageOperation: function (func) { + return this.storage.enqueueOperation(func); + }, + + /** + * Obtain persisted provider state. + * + * Provider state consists of key-value pairs of string names and values. + * Providers can stuff whatever they want into state. They are encouraged to + * store as little as possible for performance reasons. + * + * State is backed by storage and is robust. + * + * These functions do not enqueue on storage automatically, so they should + * be guarded by `enqueueStorageOperation` or some other mutex. + * + * @param key + * (string) The property to retrieve. + * + * @return Promise String value on success. null if no state + * is available under this key. + */ + getState: function (key) { + return this.storage.getProviderState(this.name, key); + }, + + /** + * Set state for this provider. + * + * This is the complementary API for `getState` and obeys the same + * storage restrictions. + */ + setState: function (key, value) { + return this.storage.setProviderState(this.name, key, value); + }, + + _dateToDays: function (date) { + return Math.floor(date.getTime() / MILLISECONDS_PER_DAY); + }, + + _daysToDate: function (days) { + return new Date(days * MILLISECONDS_PER_DAY); + }, +}); +