services/metrics/providermanager.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     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
     8 this.EXPORTED_SYMBOLS = ["ProviderManager"];
    10 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
    12 Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
    13 #endif
    15 Cu.import("resource://gre/modules/Promise.jsm");
    16 Cu.import("resource://gre/modules/Task.jsm");
    17 Cu.import("resource://gre/modules/Log.jsm");
    18 Cu.import("resource://services-common/utils.js");
    21 /**
    22  * Handles and coordinates the collection of metrics data from providers.
    23  *
    24  * This provides an interface for managing `Metrics.Provider` instances. It
    25  * provides APIs for bulk collection of data.
    26  */
    27 this.ProviderManager = function (storage) {
    28   this._log = Log.repository.getLogger("Services.Metrics.ProviderManager");
    30   this._providers = new Map();
    31   this._storage = storage;
    33   this._providerInitQueue = [];
    34   this._providerInitializing = false;
    36   this._pullOnlyProviders = {};
    37   this._pullOnlyProvidersRegisterCount = 0;
    38   this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED;
    39   this._pullOnlyProvidersCurrentPromise = null;
    41   // Callback to allow customization of providers after they are constructed
    42   // but before they call out into their initialization code.
    43   this.onProviderInit = null;
    44 }
    46 this.ProviderManager.prototype = Object.freeze({
    47   PULL_ONLY_NOT_REGISTERED: "none",
    48   PULL_ONLY_REGISTERING: "registering",
    49   PULL_ONLY_UNREGISTERING: "unregistering",
    50   PULL_ONLY_REGISTERED: "registered",
    52   get providers() {
    53     let providers = [];
    54     for (let [name, entry] of this._providers) {
    55       providers.push(entry.provider);
    56     }
    58     return providers;
    59   },
    61   /**
    62    * Obtain a provider from its name.
    63    */
    64   getProvider: function (name) {
    65     let provider = this._providers.get(name);
    67     if (!provider) {
    68       return null;
    69     }
    71     return provider.provider;
    72   },
    74   /**
    75    * Registers providers from a category manager category.
    76    *
    77    * This examines the specified category entries and registers found
    78    * providers.
    79    *
    80    * Category entries are essentially JS modules and the name of the symbol
    81    * within that module that is a `Metrics.Provider` instance.
    82    *
    83    * The category entry name is the name of the JS type for the provider. The
    84    * value is the resource:// URI to import which makes this type available.
    85    *
    86    * Example entry:
    87    *
    88    *   FooProvider resource://gre/modules/foo.jsm
    89    *
    90    * One can register entries in the application's .manifest file. e.g.
    91    *
    92    *   category healthreport-js-provider-default FooProvider resource://gre/modules/foo.jsm
    93    *   category healthreport-js-provider-nightly EyeballProvider resource://gre/modules/eyeball.jsm
    94    *
    95    * Then to load them:
    96    *
    97    *   let reporter = getHealthReporter("healthreport.");
    98    *   reporter.registerProvidersFromCategoryManager("healthreport-js-provider-default");
    99    *
   100    * If the category has no defined members, this call has no effect, and no error is raised.
   101    *
   102    * @param category
   103    *        (string) Name of category from which to query and load.
   104    * @return a newly spawned Task.
   105    */
   106   registerProvidersFromCategoryManager: function (category) {
   107     this._log.info("Registering providers from category: " + category);
   108     let cm = Cc["@mozilla.org/categorymanager;1"]
   109                .getService(Ci.nsICategoryManager);
   111     let promises = [];
   112     let enumerator = cm.enumerateCategory(category);
   113     while (enumerator.hasMoreElements()) {
   114       let entry = enumerator.getNext()
   115                             .QueryInterface(Ci.nsISupportsCString)
   116                             .toString();
   118       let uri = cm.getCategoryEntry(category, entry);
   119       this._log.info("Attempting to load provider from category manager: " +
   120                      entry + " from " + uri);
   122       try {
   123         let ns = {};
   124         Cu.import(uri, ns);
   126         let promise = this.registerProviderFromType(ns[entry]);
   127         if (promise) {
   128           promises.push(promise);
   129         }
   130       } catch (ex) {
   131         this._recordProviderError(entry,
   132                                   "Error registering provider from category manager",
   133                                   ex);
   134         continue;
   135       }
   136     }
   138     return Task.spawn(function wait() {
   139       for (let promise of promises) {
   140         yield promise;
   141       }
   142     });
   143   },
   145   /**
   146    * Registers a `MetricsProvider` with this manager.
   147    *
   148    * Once a `MetricsProvider` is registered, data will be collected from it
   149    * whenever we collect data.
   150    *
   151    * The returned value is a promise that will be resolved once registration
   152    * is complete.
   153    *
   154    * Providers are initialized as part of registration by calling
   155    * provider.init().
   156    *
   157    * @param provider
   158    *        (Metrics.Provider) The provider instance to register.
   159    *
   160    * @return Promise<null>
   161    */
   162   registerProvider: function (provider) {
   163     // We should perform an instanceof check here. However, due to merged
   164     // compartments, the Provider type may belong to one of two JSMs
   165     // isinstance gets confused depending on which module Provider comes
   166     // from. Some code references Provider from dataprovider.jsm; others from
   167     // Metrics.jsm.
   168     if (!provider.name) {
   169       throw new Error("Provider is not valid: does not have a name.");
   170     }
   171     if (this._providers.has(provider.name)) {
   172       return CommonUtils.laterTickResolvingPromise();
   173     }
   175     let deferred = Promise.defer();
   176     this._providerInitQueue.push([provider, deferred]);
   178     if (this._providerInitQueue.length == 1) {
   179       this._popAndInitProvider();
   180     }
   182     return deferred.promise;
   183   },
   185   /**
   186    * Registers a provider from its constructor function.
   187    *
   188    * If the provider is pull-only, it will be stashed away and
   189    * initialized later. Null will be returned.
   190    *
   191    * If it is not pull-only, it will be initialized immediately and a
   192    * promise will be returned. The promise will be resolved when the
   193    * provider has finished initializing.
   194    */
   195   registerProviderFromType: function (type) {
   196     let proto = type.prototype;
   197     if (proto.pullOnly) {
   198       this._log.info("Provider is pull-only. Deferring initialization: " +
   199                      proto.name);
   200       this._pullOnlyProviders[proto.name] = type;
   202       return null;
   203     }
   205     let provider = this._initProviderFromType(type);
   206     return this.registerProvider(provider);
   207   },
   209   /**
   210    * Initializes a provider from its type.
   211    *
   212    * This is how a constructor function should be turned into a provider
   213    * instance.
   214    *
   215    * A side-effect is the provider is registered with the manager.
   216    */
   217   _initProviderFromType: function (type) {
   218     let provider = new type();
   219     if (this.onProviderInit) {
   220       this.onProviderInit(provider);
   221     }
   223     return provider;
   224   },
   226   /**
   227    * Remove a named provider from the manager.
   228    *
   229    * It is the caller's responsibility to shut down the provider
   230    * instance.
   231    */
   232   unregisterProvider: function (name) {
   233     this._providers.delete(name);
   234   },
   236   /**
   237    * Ensure that pull-only providers are registered.
   238    */
   239   ensurePullOnlyProvidersRegistered: function () {
   240     let state = this._pullOnlyProvidersState;
   242     this._pullOnlyProvidersRegisterCount++;
   244     if (state == this.PULL_ONLY_REGISTERED) {
   245       this._log.debug("Requested pull-only provider registration and " +
   246                       "providers are already registered.");
   247       return CommonUtils.laterTickResolvingPromise();
   248     }
   250     // If we're in the process of registering, chain off that request.
   251     if (state == this.PULL_ONLY_REGISTERING) {
   252       this._log.debug("Requested pull-only provider registration and " +
   253                       "registration is already in progress.");
   254       return this._pullOnlyProvidersCurrentPromise;
   255     }
   257     this._log.debug("Pull-only provider registration requested.");
   259     // A side-effect of setting this is that an active unregistration will
   260     // effectively short circuit and finish as soon as the in-flight
   261     // unregistration (if any) finishes.
   262     this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERING;
   264     let inFlightPromise = this._pullOnlyProvidersCurrentPromise;
   266     this._pullOnlyProvidersCurrentPromise =
   267       Task.spawn(function registerPullProviders() {
   269       if (inFlightPromise) {
   270         this._log.debug("Waiting for in-flight pull-only provider activity " +
   271                         "to finish before registering.");
   272         try {
   273           yield inFlightPromise;
   274         } catch (ex) {
   275           this._log.warn("Error when waiting for existing pull-only promise: " +
   276                          CommonUtils.exceptionStr(ex));
   277         }
   278       }
   280       for each (let providerType in this._pullOnlyProviders) {
   281         // Short-circuit if we're no longer registering.
   282         if (this._pullOnlyProvidersState != this.PULL_ONLY_REGISTERING) {
   283           this._log.debug("Aborting pull-only provider registration.");
   284           break;
   285         }
   287         try {
   288           let provider = this._initProviderFromType(providerType);
   290           // This is a no-op if the provider is already registered. So, the
   291           // only overhead is constructing an instance. This should be cheap
   292           // and isn't worth optimizing.
   293           yield this.registerProvider(provider);
   294         } catch (ex) {
   295           this._recordProviderError(providerType.prototype.name,
   296                                     "Error registering pull-only provider",
   297                                     ex);
   298         }
   299       }
   301       // It's possible we changed state while registering. Only mark as
   302       // registered if we didn't change state.
   303       if (this._pullOnlyProvidersState == this.PULL_ONLY_REGISTERING) {
   304         this._pullOnlyProvidersState = this.PULL_ONLY_REGISTERED;
   305         this._pullOnlyProvidersCurrentPromise = null;
   306       }
   307     }.bind(this));
   308     return this._pullOnlyProvidersCurrentPromise;
   309   },
   311   ensurePullOnlyProvidersUnregistered: function () {
   312     let state = this._pullOnlyProvidersState;
   314     // If we're not registered, this is a no-op.
   315     if (state == this.PULL_ONLY_NOT_REGISTERED) {
   316       this._log.debug("Requested pull-only provider unregistration but none " +
   317                       "are registered.");
   318       return CommonUtils.laterTickResolvingPromise();
   319     }
   321     // If we're currently unregistering, recycle the promise from last time.
   322     if (state == this.PULL_ONLY_UNREGISTERING) {
   323       this._log.debug("Requested pull-only provider unregistration and " +
   324                  "unregistration is in progress.");
   325       this._pullOnlyProvidersRegisterCount =
   326         Math.max(0, this._pullOnlyProvidersRegisterCount - 1);
   328       return this._pullOnlyProvidersCurrentPromise;
   329     }
   331     // We ignore this request while multiple entities have requested
   332     // registration because we don't want a request from an "inner,"
   333     // short-lived request to overwrite the desire of the "parent,"
   334     // longer-lived request.
   335     if (this._pullOnlyProvidersRegisterCount > 1) {
   336       this._log.debug("Requested pull-only provider unregistration while " +
   337                       "other callers still want them registered. Ignoring.");
   338       this._pullOnlyProvidersRegisterCount--;
   339       return CommonUtils.laterTickResolvingPromise();
   340     }
   342     // We are either fully registered or registering with a single consumer.
   343     // In both cases we are authoritative and can commence unregistration.
   345     this._log.debug("Pull-only providers being unregistered.");
   346     this._pullOnlyProvidersRegisterCount =
   347       Math.max(0, this._pullOnlyProvidersRegisterCount - 1);
   348     this._pullOnlyProvidersState = this.PULL_ONLY_UNREGISTERING;
   349     let inFlightPromise = this._pullOnlyProvidersCurrentPromise;
   351     this._pullOnlyProvidersCurrentPromise =
   352       Task.spawn(function unregisterPullProviders() {
   354       if (inFlightPromise) {
   355         this._log.debug("Waiting for in-flight pull-only provider activity " +
   356                         "to complete before unregistering.");
   357         try {
   358           yield inFlightPromise;
   359         } catch (ex) {
   360           this._log.warn("Error when waiting for existing pull-only promise: " +
   361                          CommonUtils.exceptionStr(ex));
   362         }
   363       }
   365       for (let provider of this.providers) {
   366         if (this._pullOnlyProvidersState != this.PULL_ONLY_UNREGISTERING) {
   367           return;
   368         }
   370         if (!provider.pullOnly) {
   371           continue;
   372         }
   374         this._log.info("Shutting down pull-only provider: " +
   375                        provider.name);
   377         try {
   378           yield provider.shutdown();
   379         } catch (ex) {
   380           this._recordProviderError(provider.name,
   381                                     "Error when shutting down provider",
   382                                     ex);
   383         } finally {
   384           this.unregisterProvider(provider.name);
   385         }
   386       }
   388       if (this._pullOnlyProvidersState == this.PULL_ONLY_UNREGISTERING) {
   389         this._pullOnlyProvidersState = this.PULL_ONLY_NOT_REGISTERED;
   390         this._pullOnlyProvidersCurrentPromise = null;
   391       }
   392     }.bind(this));
   393     return this._pullOnlyProvidersCurrentPromise;
   394   },
   396   _popAndInitProvider: function () {
   397     if (!this._providerInitQueue.length || this._providerInitializing) {
   398       return;
   399     }
   401     let [provider, deferred] = this._providerInitQueue.shift();
   402     this._providerInitializing = true;
   404     this._log.info("Initializing provider with storage: " + provider.name);
   406     Task.spawn(function initProvider() {
   407       try {
   408         let result = yield provider.init(this._storage);
   409         this._log.info("Provider successfully initialized: " + provider.name);
   411         this._providers.set(provider.name, {
   412           provider: provider,
   413           constantsCollected: false,
   414         });
   416         deferred.resolve(result);
   417       } catch (ex) {
   418         this._recordProviderError(provider.name, "Failed to initialize", ex);
   419         deferred.reject(ex);
   420       } finally {
   421         this._providerInitializing = false;
   422         this._popAndInitProvider();
   423       }
   424     }.bind(this));
   425   },
   427   /**
   428    * Collects all constant measurements from all providers.
   429    *
   430    * Returns a Promise that will be fulfilled once all data providers have
   431    * provided their constant data. A side-effect of this promise fulfillment
   432    * is that the manager is populated with the obtained collection results.
   433    * The resolved value to the promise is this `ProviderManager` instance.
   434    */
   435   collectConstantData: function () {
   436     let entries = [];
   438     for (let [name, entry] of this._providers) {
   439       if (entry.constantsCollected) {
   440         this._log.trace("Provider has already provided constant data: " +
   441                         name);
   442         continue;
   443       }
   445       entries.push(entry);
   446     }
   448     let onCollect = function (entry, result) {
   449       entry.constantsCollected = true;
   450     };
   452     return this._callCollectOnProviders(entries, "collectConstantData",
   453                                         onCollect);
   454   },
   456   /**
   457    * Calls collectDailyData on all providers.
   458    */
   459   collectDailyData: function () {
   460     return this._callCollectOnProviders(this._providers.values(),
   461                                         "collectDailyData");
   462   },
   464   _callCollectOnProviders: function (entries, fnProperty, onCollect=null) {
   465     let promises = [];
   467     for (let entry of entries) {
   468       let provider = entry.provider;
   469       let collectPromise;
   470       try {
   471         collectPromise = provider[fnProperty].call(provider);
   472       } catch (ex) {
   473         this._recordProviderError(provider.name, "Exception when calling " +
   474                                   "collect function: " + fnProperty, ex);
   475         continue;
   476       }
   478       if (!collectPromise) {
   479         this._recordProviderError(provider.name, "Does not return a promise " +
   480                                   "from " + fnProperty + "()");
   481         continue;
   482       }
   484       let promise = collectPromise.then(function onCollected(result) {
   485         if (onCollect) {
   486           try {
   487             onCollect(entry, result);
   488           } catch (ex) {
   489             this._log.warn("onCollect callback threw: " +
   490                            CommonUtils.exceptionStr(ex));
   491           }
   492         }
   494         return CommonUtils.laterTickResolvingPromise(result);
   495       });
   497       promises.push([provider.name, promise]);
   498     }
   500     return this._handleCollectionPromises(promises);
   501   },
   503   /**
   504    * Handles promises returned by the collect* functions.
   505    *
   506    * This consumes the data resolved by the promises and returns a new promise
   507    * that will be resolved once all promises have been resolved.
   508    *
   509    * The promise is resolved even if one of the underlying collection
   510    * promises is rejected.
   511    */
   512   _handleCollectionPromises: function (promises) {
   513     return Task.spawn(function waitForPromises() {
   514       for (let [name, promise] of promises) {
   515         try {
   516           yield promise;
   517           this._log.debug("Provider collected successfully: " + name);
   518         } catch (ex) {
   519           this._recordProviderError(name, "Failed to collect", ex);
   520         }
   521       }
   523       throw new Task.Result(this);
   524     }.bind(this));
   525   },
   527   /**
   528    * Record an error that occurred operating on a provider.
   529    */
   530   _recordProviderError: function (name, msg, ex) {
   531     let msg = "Provider error: " + name + ": " + msg;
   532     if (ex) {
   533       msg += ": " + CommonUtils.exceptionStr(ex);
   534     }
   535     this._log.warn(msg);
   537     if (this.onProviderError) {
   538       try {
   539         this.onProviderError(msg);
   540       } catch (callError) {
   541         this._log.warn("Exception when calling onProviderError callback: " +
   542                        CommonUtils.exceptionStr(callError));
   543       }
   544     }
   545   },
   546 });

mercurial