Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
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 });