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