OLD | NEW |
(Empty) | |
| 1 // Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file |
| 2 // for details. All rights reserved. Use of this source code is governed by a |
| 3 // BSD-style license that can be found in the LICENSE file. |
| 4 |
| 5 /// This library enables one to create a service scope in which code can run. |
| 6 /// |
| 7 /// A service scope is an environment in which code runs. The environment is a |
| 8 /// [Zone] with added functionality. Code can be run inside a new service scope |
| 9 /// by using the `fork(callback)` method. This will call `callback` inside a new |
| 10 /// service scope and will keep the scope alive until the Future returned by the |
| 11 /// callback completes. At this point the service scope ends. |
| 12 /// |
| 13 /// Code running inside a new service scope can |
| 14 /// |
| 15 /// - register objects (e.g. a database connection pool or a logging service) |
| 16 /// - look up previously registered objects |
| 17 /// - register on-scope-exit handlers |
| 18 /// |
| 19 /// Service scopes can be nested. All registered values from the parent service |
| 20 /// scope are still accessible as long as they have not been overridden. The |
| 21 /// callback passed to `fork()` is responsible for not completing it's returned |
| 22 /// Future until all nested service scopes have ended. |
| 23 /// |
| 24 /// The on-scope-exit callbacks will be called when the service scope ends. The |
| 25 /// callbacks are run in reverse registration order and are guaranteed to be |
| 26 /// executed. During a scope exit callback the active service scope cannot |
| 27 /// be modified anymore and `lookup()`s will only return values which were |
| 28 /// registered before the registration of the on-scope-exit callback. |
| 29 /// |
| 30 /// One use-case of this is making services available to a server application. |
| 31 /// The server application will run inside a service scope which will have all |
| 32 /// necessary services registered. |
| 33 /// Once the server app shuts down, the registered on-scope-exit callbacks will |
| 34 /// automatically be invoked and the process will shut down cleanly. |
| 35 /// |
| 36 /// Here is an example use case: |
| 37 /// |
| 38 /// import 'dart:async'; |
| 39 /// import 'package:gcloud/service_scope.dart' as scope; |
| 40 /// |
| 41 /// class DBPool { ... } |
| 42 /// |
| 43 /// DBPool get dbService => scope.lookup(#dbpool); |
| 44 /// |
| 45 /// Future runApp() { |
| 46 /// // The application can use the registered objects (here the |
| 47 /// // dbService). It does not need to pass it around, but can use a |
| 48 /// // global getter. |
| 49 /// return dbService.query( ... ).listen(print).asFuture(); |
| 50 /// } |
| 51 /// |
| 52 /// main() { |
| 53 /// // Creates a new service scope and runs the given closure inside it. |
| 54 /// ss.fork(() { |
| 55 /// // We create a new database pool with a 10 active connections and |
| 56 /// // add it to the current service scope with key `#dbpool`. |
| 57 /// // In addition we insert a on-scope-exit callback which will be |
| 58 /// // called once the application is done. |
| 59 /// var pool = new DBPool(connections: 10); |
| 60 /// scope.register(#dbpool, pool, onScopeExit: () => pool.close()); |
| 61 /// return runApp(); |
| 62 /// }).then((_) { |
| 63 /// print('Server application shut down cleanly'); |
| 64 /// }); |
| 65 /// } |
| 66 /// |
| 67 /// As an example, the `package:appengine/appengine.dart` package runs request |
| 68 /// handlers inside a service scope, which has most `package:gcloud` services |
| 69 /// registered. |
| 70 /// |
| 71 /// The core application code can then be independent of `package:appengine` |
| 72 /// and instead depend only on the services needed (e.g. |
| 73 /// `package:gcloud/storage.dart`) by using getters in the service library (e.g. |
| 74 /// the `storageService`) which are implemented with service scope lookups. |
| 75 library gcloud.service_scope; |
| 76 |
| 77 import 'dart:async'; |
| 78 |
| 79 /// The Symbol used as index in the zone map for the service scope object. |
| 80 const Symbol _ServiceScopeKey = #_gcloud.service_scope; |
| 81 |
| 82 /// An empty service scope. |
| 83 /// |
| 84 /// New service scope can be created by calling [fork] on the empty |
| 85 /// service scope. |
| 86 final _ServiceScope _emptyServiceScope = new _ServiceScope(); |
| 87 |
| 88 /// Returns the current [_ServiceScope] object. |
| 89 _ServiceScope get _serviceScope => Zone.current[_ServiceScopeKey]; |
| 90 |
| 91 /// Start a new zone with a new service scope and run [func] inside it. |
| 92 /// |
| 93 /// The function [func] must return a `Future` and the service scope will end |
| 94 /// when this future completes. |
| 95 /// |
| 96 /// If an uncaught error occurs and [onError] is given, it will be called. The |
| 97 /// `onError` parameter can take the same values as `Zone.current.fork`. |
| 98 Future fork(Future func(), {Function onError}) { |
| 99 var currentServiceScope = _serviceScope; |
| 100 if (currentServiceScope == null) { |
| 101 currentServiceScope = _emptyServiceScope; |
| 102 } |
| 103 return currentServiceScope._fork(func, onError: onError); |
| 104 } |
| 105 |
| 106 /// Register a new [object] into the current service scope using the given |
| 107 /// [key]. |
| 108 /// |
| 109 /// If [onScopeExit] is provided, it will be called when the service scope ends. |
| 110 /// |
| 111 /// The registered on-scope-exit functions are executed in reverse registration |
| 112 /// order. |
| 113 void register(Object key, Object value, {onScopeExit()}) { |
| 114 var serviceScope = _serviceScope; |
| 115 if (serviceScope == null) { |
| 116 throw new StateError('Not running inside a service scope zone.'); |
| 117 } |
| 118 serviceScope.register(key, value, onScopeExit: onScopeExit); |
| 119 } |
| 120 |
| 121 /// Register a [onScopeExitCallback] to be invoked when this service scope ends. |
| 122 /// |
| 123 /// The registered on-scope-exit functions are executed in reverse registration |
| 124 /// order. |
| 125 Object registerScopeExitCallback(onScopeExitCallback()) { |
| 126 var serviceScope = _serviceScope; |
| 127 if (serviceScope == null) { |
| 128 throw new StateError('Not running inside a service scope zone.'); |
| 129 } |
| 130 return serviceScope.registerOnScopeExitCallback(onScopeExitCallback); |
| 131 } |
| 132 |
| 133 /// Look up an item by it's key in the currently active service scope. |
| 134 /// |
| 135 /// Returns `null` if there is no entry with the given key. |
| 136 Object lookup(Object key) { |
| 137 var serviceScope = _serviceScope; |
| 138 if (serviceScope == null) { |
| 139 throw new StateError('Not running inside a service scope zone.'); |
| 140 } |
| 141 return serviceScope.lookup(key); |
| 142 } |
| 143 |
| 144 /// Represents a global service scope of values stored via zones. |
| 145 class _ServiceScope { |
| 146 /// A mapping of keys to values stored inside the service scope. |
| 147 final Map<Object, Object> _key2Values = new Map<Object, Object>(); |
| 148 |
| 149 /// A set which indicates whether an object was copied from it's parent. |
| 150 final Set<Object> _parentCopies = new Set<Object>(); |
| 151 |
| 152 /// On-Scope-Exit functions which will be called in reverse insertion order. |
| 153 final List<_RegisteredEntry> _registeredEntries = []; |
| 154 |
| 155 bool _cleaningUp = false; |
| 156 bool _destroyed = false; |
| 157 |
| 158 /// Looks up an object by it's service scope key - returns `null` if not |
| 159 /// found. |
| 160 Object lookup(Object serviceScope) { |
| 161 _ensureNotInDestroyingState(); |
| 162 var entry = _key2Values[serviceScope]; |
| 163 return entry != null ? entry.value : null; |
| 164 } |
| 165 |
| 166 /// Inserts a new item to the service scope using [serviceScopeKey]. |
| 167 /// |
| 168 /// Optionally calls a [onScopeExit] function once this service scope ends. |
| 169 void register(Object serviceScopeKey, Object value, {onScopeExit()}) { |
| 170 _ensureNotInCleaningState(); |
| 171 _ensureNotInDestroyingState(); |
| 172 |
| 173 bool isParentCopy = _parentCopies.contains(serviceScopeKey); |
| 174 if (!isParentCopy && _key2Values.containsKey(serviceScopeKey)) { |
| 175 throw new ArgumentError( |
| 176 'Servie scope already contains key $serviceScopeKey.'); |
| 177 } |
| 178 |
| 179 var entry = new _RegisteredEntry(serviceScopeKey, value, onScopeExit); |
| 180 |
| 181 _key2Values[serviceScopeKey] = entry; |
| 182 if (isParentCopy) _parentCopies.remove(serviceScopeKey); |
| 183 |
| 184 _registeredEntries.add(entry); |
| 185 } |
| 186 |
| 187 /// Inserts a new on-scope-exit function to be called once this service scope |
| 188 /// ends. |
| 189 void registerOnScopeExitCallback(onScopeExitCallback()) { |
| 190 _ensureNotInCleaningState(); |
| 191 _ensureNotInDestroyingState(); |
| 192 |
| 193 if (onScopeExitCallback != null) { |
| 194 _registeredEntries.add( |
| 195 new _RegisteredEntry(null, null, onScopeExitCallback)); |
| 196 } |
| 197 } |
| 198 |
| 199 /// Start a new zone with a forked service scope. |
| 200 Future _fork(Future func(), {Function onError}) { |
| 201 _ensureNotInCleaningState(); |
| 202 _ensureNotInDestroyingState(); |
| 203 |
| 204 var serviceScope = _copy(); |
| 205 var map = { _ServiceScopeKey: serviceScope }; |
| 206 return runZoned(() { |
| 207 var f = func(); |
| 208 if (f is! Future) { |
| 209 throw new ArgumentError('Forking a service scope zone requires the ' |
| 210 'callback function to return a future.'); |
| 211 } |
| 212 return f.whenComplete(serviceScope._runScopeExitHandlers); |
| 213 }, zoneValues: map, onError: onError); |
| 214 } |
| 215 |
| 216 void _ensureNotInDestroyingState() { |
| 217 if (_destroyed) { |
| 218 throw new StateError( |
| 219 'The service scope has already been exited. It is therefore ' |
| 220 'forbidden to use this service scope anymore. ' |
| 221 'Please make sure that your code waits for all asynchronous tasks ' |
| 222 'before the closure passed to fork() completes.'); |
| 223 } |
| 224 } |
| 225 |
| 226 void _ensureNotInCleaningState() { |
| 227 if (_cleaningUp) { |
| 228 throw new StateError( |
| 229 'The service scope is in the process of cleaning up. It is therefore ' |
| 230 'forbidden to make any modifications to the current service scope. ' |
| 231 'Please make sure that your code waits for all asynchronous tasks ' |
| 232 'before the closure passed to fork() completes.'); |
| 233 } |
| 234 } |
| 235 |
| 236 /// Copies all service scope entries to a new service scope, but not their |
| 237 /// on-scope-exit handlers. |
| 238 _ServiceScope _copy() { |
| 239 var serviceScopeCopy = new _ServiceScope(); |
| 240 serviceScopeCopy._key2Values.addAll(_key2Values); |
| 241 serviceScopeCopy._parentCopies.addAll(_key2Values.keys); |
| 242 return serviceScopeCopy; |
| 243 } |
| 244 |
| 245 /// Runs all on-scope-exit functions in [_ServiceScope]. |
| 246 Future _runScopeExitHandlers() { |
| 247 _cleaningUp = true; |
| 248 var errors = []; |
| 249 |
| 250 // We are running all on-scope-exit functions in reverse registration order. |
| 251 // Even if one fails, we continue cleaning up and report then the list of |
| 252 // errors (if there were any). |
| 253 return Future.forEach(_registeredEntries.reversed, |
| 254 (_RegisteredEntry registeredEntry) { |
| 255 if (registeredEntry.key != null) { |
| 256 _key2Values.remove(registeredEntry.key); |
| 257 } |
| 258 if (registeredEntry.scopeExitCallback != null) { |
| 259 return new Future.sync(registeredEntry.scopeExitCallback) |
| 260 .catchError((e, s) => errors.add(e)); |
| 261 } else { |
| 262 return new Future.value(); |
| 263 } |
| 264 }).then((_) { |
| 265 _cleaningUp = true; |
| 266 _destroyed = true; |
| 267 if (errors.length > 0) { |
| 268 throw new Exception( |
| 269 'The following errors occured while running scope exit handlers' |
| 270 ': $errors'); |
| 271 } |
| 272 }); |
| 273 } |
| 274 } |
| 275 |
| 276 class _RegisteredEntry { |
| 277 final Object key; |
| 278 final Object value; |
| 279 final Function scopeExitCallback; |
| 280 |
| 281 _RegisteredEntry(this.key, this.value, this.scopeExitCallback); |
| 282 } |
OLD | NEW |