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); |
+} |