| Index: pkg/gcloud/lib/service_scope.dart
|
| diff --git a/pkg/gcloud/lib/service_scope.dart b/pkg/gcloud/lib/service_scope.dart
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..856365c77de4b7f1164857e86dd51c56bb6eac7a
|
| --- /dev/null
|
| +++ b/pkg/gcloud/lib/service_scope.dart
|
| @@ -0,0 +1,282 @@
|
| +// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file
|
| +// for details. All rights reserved. Use of this source code is governed by a
|
| +// BSD-style license that can be found in the LICENSE file.
|
| +
|
| +/// This library enables one to create a service scope in which code can run.
|
| +///
|
| +/// A service scope is an environment in which code runs. The environment is a
|
| +/// [Zone] with added functionality. Code can be run inside a new service scope
|
| +/// by using the `fork(callback)` method. This will call `callback` inside a new
|
| +/// service scope and will keep the scope alive until the Future returned by the
|
| +/// callback completes. At this point the service scope ends.
|
| +///
|
| +/// Code running inside a new service scope can
|
| +///
|
| +/// - register objects (e.g. a database connection pool or a logging service)
|
| +/// - look up previously registered objects
|
| +/// - register on-scope-exit handlers
|
| +///
|
| +/// Service scopes can be nested. All registered values from the parent service
|
| +/// scope are still accessible as long as they have not been overridden. The
|
| +/// callback passed to `fork()` is responsible for not completing it's returned
|
| +/// Future until all nested service scopes have ended.
|
| +///
|
| +/// The on-scope-exit callbacks will be called when the service scope ends. The
|
| +/// callbacks are run in reverse registration order and are guaranteed to be
|
| +/// executed. During a scope exit callback the active service scope cannot
|
| +/// be modified anymore and `lookup()`s will only return values which were
|
| +/// registered before the registration of the on-scope-exit callback.
|
| +///
|
| +/// One use-case of this is making services available to a server application.
|
| +/// The server application will run inside a service scope which will have all
|
| +/// necessary services registered.
|
| +/// Once the server app shuts down, the registered on-scope-exit callbacks will
|
| +/// automatically be invoked and the process will shut down cleanly.
|
| +///
|
| +/// Here is an example use case:
|
| +///
|
| +/// import 'dart:async';
|
| +/// import 'package:gcloud/service_scope.dart' as scope;
|
| +///
|
| +/// class DBPool { ... }
|
| +///
|
| +/// DBPool get dbService => scope.lookup(#dbpool);
|
| +///
|
| +/// Future runApp() {
|
| +/// // The application can use the registered objects (here the
|
| +/// // dbService). It does not need to pass it around, but can use a
|
| +/// // global getter.
|
| +/// return dbService.query( ... ).listen(print).asFuture();
|
| +/// }
|
| +///
|
| +/// main() {
|
| +/// // Creates a new service scope and runs the given closure inside it.
|
| +/// ss.fork(() {
|
| +/// // We create a new database pool with a 10 active connections and
|
| +/// // add it to the current service scope with key `#dbpool`.
|
| +/// // In addition we insert a on-scope-exit callback which will be
|
| +/// // called once the application is done.
|
| +/// var pool = new DBPool(connections: 10);
|
| +/// scope.register(#dbpool, pool, onScopeExit: () => pool.close());
|
| +/// return runApp();
|
| +/// }).then((_) {
|
| +/// print('Server application shut down cleanly');
|
| +/// });
|
| +/// }
|
| +///
|
| +/// As an example, the `package:appengine/appengine.dart` package runs request
|
| +/// handlers inside a service scope, which has most `package:gcloud` services
|
| +/// registered.
|
| +///
|
| +/// The core application code can then be independent of `package:appengine`
|
| +/// and instead depend only on the services needed (e.g.
|
| +/// `package:gcloud/storage.dart`) by using getters in the service library (e.g.
|
| +/// the `storageService`) which are implemented with service scope lookups.
|
| +library gcloud.service_scope;
|
| +
|
| +import 'dart:async';
|
| +
|
| +/// The Symbol used as index in the zone map for the service scope object.
|
| +const Symbol _ServiceScopeKey = #_gcloud.service_scope;
|
| +
|
| +/// An empty service scope.
|
| +///
|
| +/// New service scope can be created by calling [fork] on the empty
|
| +/// service scope.
|
| +final _ServiceScope _emptyServiceScope = new _ServiceScope();
|
| +
|
| +/// Returns the current [_ServiceScope] object.
|
| +_ServiceScope get _serviceScope => Zone.current[_ServiceScopeKey];
|
| +
|
| +/// Start a new zone with a new service scope and run [func] inside it.
|
| +///
|
| +/// The function [func] must return a `Future` and the service scope will end
|
| +/// when this future completes.
|
| +///
|
| +/// If an uncaught error occurs and [onError] is given, it will be called. The
|
| +/// `onError` parameter can take the same values as `Zone.current.fork`.
|
| +Future fork(Future func(), {Function onError}) {
|
| + var currentServiceScope = _serviceScope;
|
| + if (currentServiceScope == null) {
|
| + currentServiceScope = _emptyServiceScope;
|
| + }
|
| + return currentServiceScope._fork(func, onError: onError);
|
| +}
|
| +
|
| +/// Register a new [object] into the current service scope using the given
|
| +/// [key].
|
| +///
|
| +/// If [onScopeExit] is provided, it will be called when the service scope ends.
|
| +///
|
| +/// The registered on-scope-exit functions are executed in reverse registration
|
| +/// order.
|
| +void register(Object key, Object value, {onScopeExit()}) {
|
| + var serviceScope = _serviceScope;
|
| + if (serviceScope == null) {
|
| + throw new StateError('Not running inside a service scope zone.');
|
| + }
|
| + serviceScope.register(key, value, onScopeExit: onScopeExit);
|
| +}
|
| +
|
| +/// Register a [onScopeExitCallback] to be invoked when this service scope ends.
|
| +///
|
| +/// The registered on-scope-exit functions are executed in reverse registration
|
| +/// order.
|
| +Object registerScopeExitCallback(onScopeExitCallback()) {
|
| + var serviceScope = _serviceScope;
|
| + if (serviceScope == null) {
|
| + throw new StateError('Not running inside a service scope zone.');
|
| + }
|
| + return serviceScope.registerOnScopeExitCallback(onScopeExitCallback);
|
| +}
|
| +
|
| +/// Look up an item by it's key in the currently active service scope.
|
| +///
|
| +/// Returns `null` if there is no entry with the given key.
|
| +Object lookup(Object key) {
|
| + var serviceScope = _serviceScope;
|
| + if (serviceScope == null) {
|
| + throw new StateError('Not running inside a service scope zone.');
|
| + }
|
| + return serviceScope.lookup(key);
|
| +}
|
| +
|
| +/// Represents a global service scope of values stored via zones.
|
| +class _ServiceScope {
|
| + /// A mapping of keys to values stored inside the service scope.
|
| + final Map<Object, Object> _key2Values = new Map<Object, Object>();
|
| +
|
| + /// A set which indicates whether an object was copied from it's parent.
|
| + final Set<Object> _parentCopies = new Set<Object>();
|
| +
|
| + /// On-Scope-Exit functions which will be called in reverse insertion order.
|
| + final List<_RegisteredEntry> _registeredEntries = [];
|
| +
|
| + bool _cleaningUp = false;
|
| + bool _destroyed = false;
|
| +
|
| + /// Looks up an object by it's service scope key - returns `null` if not
|
| + /// found.
|
| + Object lookup(Object serviceScope) {
|
| + _ensureNotInDestroyingState();
|
| + var entry = _key2Values[serviceScope];
|
| + return entry != null ? entry.value : null;
|
| + }
|
| +
|
| + /// Inserts a new item to the service scope using [serviceScopeKey].
|
| + ///
|
| + /// Optionally calls a [onScopeExit] function once this service scope ends.
|
| + void register(Object serviceScopeKey, Object value, {onScopeExit()}) {
|
| + _ensureNotInCleaningState();
|
| + _ensureNotInDestroyingState();
|
| +
|
| + bool isParentCopy = _parentCopies.contains(serviceScopeKey);
|
| + if (!isParentCopy && _key2Values.containsKey(serviceScopeKey)) {
|
| + throw new ArgumentError(
|
| + 'Servie scope already contains key $serviceScopeKey.');
|
| + }
|
| +
|
| + var entry = new _RegisteredEntry(serviceScopeKey, value, onScopeExit);
|
| +
|
| + _key2Values[serviceScopeKey] = entry;
|
| + if (isParentCopy) _parentCopies.remove(serviceScopeKey);
|
| +
|
| + _registeredEntries.add(entry);
|
| + }
|
| +
|
| + /// Inserts a new on-scope-exit function to be called once this service scope
|
| + /// ends.
|
| + void registerOnScopeExitCallback(onScopeExitCallback()) {
|
| + _ensureNotInCleaningState();
|
| + _ensureNotInDestroyingState();
|
| +
|
| + if (onScopeExitCallback != null) {
|
| + _registeredEntries.add(
|
| + new _RegisteredEntry(null, null, onScopeExitCallback));
|
| + }
|
| + }
|
| +
|
| + /// Start a new zone with a forked service scope.
|
| + Future _fork(Future func(), {Function onError}) {
|
| + _ensureNotInCleaningState();
|
| + _ensureNotInDestroyingState();
|
| +
|
| + var serviceScope = _copy();
|
| + var map = { _ServiceScopeKey: serviceScope };
|
| + return runZoned(() {
|
| + var f = func();
|
| + if (f is! Future) {
|
| + throw new ArgumentError('Forking a service scope zone requires the '
|
| + 'callback function to return a future.');
|
| + }
|
| + return f.whenComplete(serviceScope._runScopeExitHandlers);
|
| + }, zoneValues: map, onError: onError);
|
| + }
|
| +
|
| + void _ensureNotInDestroyingState() {
|
| + if (_destroyed) {
|
| + throw new StateError(
|
| + 'The service scope has already been exited. It is therefore '
|
| + 'forbidden to use this service scope anymore. '
|
| + 'Please make sure that your code waits for all asynchronous tasks '
|
| + 'before the closure passed to fork() completes.');
|
| + }
|
| + }
|
| +
|
| + void _ensureNotInCleaningState() {
|
| + if (_cleaningUp) {
|
| + throw new StateError(
|
| + 'The service scope is in the process of cleaning up. It is therefore '
|
| + 'forbidden to make any modifications to the current service scope. '
|
| + 'Please make sure that your code waits for all asynchronous tasks '
|
| + 'before the closure passed to fork() completes.');
|
| + }
|
| + }
|
| +
|
| + /// Copies all service scope entries to a new service scope, but not their
|
| + /// on-scope-exit handlers.
|
| + _ServiceScope _copy() {
|
| + var serviceScopeCopy = new _ServiceScope();
|
| + serviceScopeCopy._key2Values.addAll(_key2Values);
|
| + serviceScopeCopy._parentCopies.addAll(_key2Values.keys);
|
| + return serviceScopeCopy;
|
| + }
|
| +
|
| + /// Runs all on-scope-exit functions in [_ServiceScope].
|
| + Future _runScopeExitHandlers() {
|
| + _cleaningUp = true;
|
| + var errors = [];
|
| +
|
| + // We are running all on-scope-exit functions in reverse registration order.
|
| + // Even if one fails, we continue cleaning up and report then the list of
|
| + // errors (if there were any).
|
| + return Future.forEach(_registeredEntries.reversed,
|
| + (_RegisteredEntry registeredEntry) {
|
| + if (registeredEntry.key != null) {
|
| + _key2Values.remove(registeredEntry.key);
|
| + }
|
| + if (registeredEntry.scopeExitCallback != null) {
|
| + return new Future.sync(registeredEntry.scopeExitCallback)
|
| + .catchError((e, s) => errors.add(e));
|
| + } else {
|
| + return new Future.value();
|
| + }
|
| + }).then((_) {
|
| + _cleaningUp = true;
|
| + _destroyed = true;
|
| + if (errors.length > 0) {
|
| + throw new Exception(
|
| + 'The following errors occured while running scope exit handlers'
|
| + ': $errors');
|
| + }
|
| + });
|
| + }
|
| +}
|
| +
|
| +class _RegisteredEntry {
|
| + final Object key;
|
| + final Object value;
|
| + final Function scopeExitCallback;
|
| +
|
| + _RegisteredEntry(this.key, this.value, this.scopeExitCallback);
|
| +}
|
|
|