Line data Source code
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 : import 'dart:async';
6 : import 'dart:collection';
7 :
8 : import 'package:async/async.dart';
9 : import 'package:stack_trace/stack_trace.dart';
10 :
11 : /// Manages an abstract pool of resources with a limit on how many may be in use
12 : /// at once.
13 : ///
14 : /// When a resource is needed, the user should call [request]. When the returned
15 : /// future completes with a [PoolResource], the resource may be allocated. Once
16 : /// the resource has been released, the user should call [PoolResource.release].
17 : /// The pool will ensure that only a certain number of [PoolResource]s may be
18 : /// allocated at once.
19 : class Pool {
20 : /// Completers for requests beyond the first [_maxAllocatedResources].
21 : ///
22 : /// When an item is released, the next element of [_requestedResources] will
23 : /// be completed.
24 : final _requestedResources = new Queue<Completer<PoolResource>>();
25 :
26 : /// Callbacks that must be called before additional resources can be
27 : /// allocated.
28 : ///
29 : /// See [PoolResource.allowRelease].
30 : final _onReleaseCallbacks = new Queue<Function>();
31 :
32 : /// Completers that will be completed once `onRelease` callbacks are done
33 : /// running.
34 : ///
35 : /// These are kept in a queue to ensure that the earliest request completes
36 : /// first regardless of what order the `onRelease` callbacks complete in.
37 : final _onReleaseCompleters = new Queue<Completer<PoolResource>>();
38 :
39 : /// The maximum number of resources that may be allocated at once.
40 : final int _maxAllocatedResources;
41 :
42 : /// The number of resources that are currently allocated.
43 : int _allocatedResources = 0;
44 :
45 : /// The timeout timer.
46 : ///
47 : /// This timer is canceled as long as the pool is below the resource limit.
48 : /// It's reset once the resource limit is reached and again every time an
49 : /// resource is released or a new resource is requested. If it fires, that
50 : /// indicates that the caller became deadlocked, likely due to files waiting
51 : /// for additional files to be read before they could be closed.
52 : ///
53 : /// This is `null` if this pool shouldn't time out.
54 : RestartableTimer _timer;
55 :
56 : /// The amount of time to wait before timing out the pending resources.
57 : final Duration _timeout;
58 :
59 : /// A [FutureGroup] that tracks all the `onRelease` callbacks for resources
60 : /// that have been marked releasable.
61 : ///
62 : /// This is `null` until [close] is called.
63 : FutureGroup _closeGroup;
64 :
65 : /// Whether [close] has been called.
66 10 : bool get isClosed => _closeMemo.hasRun;
67 :
68 : /// A future that completes once the pool is closed and all its outstanding
69 : /// resources have been released.
70 : ///
71 : /// If any [PoolResource.allowRelease] callback throws an exception after the
72 : /// pool is closed, this completes with that exception.
73 10 : Future get done => _closeMemo.future;
74 :
75 : /// Creates a new pool with the given limit on how many resources may be
76 : /// allocated at once.
77 : ///
78 : /// If [timeout] is passed, then if that much time passes without any activity
79 : /// all pending [request] futures will throw a [TimeoutException]. This is
80 : /// intended to avoid deadlocks.
81 5 : Pool(this._maxAllocatedResources, {Duration timeout}) : _timeout = timeout {
82 : if (timeout != null) {
83 : // Start the timer canceled since we only want to start counting down once
84 : // we've run out of available resources.
85 0 : _timer = new RestartableTimer(timeout, _onTimeout)..cancel();
86 : }
87 : }
88 :
89 : /// Request a [PoolResource].
90 : ///
91 : /// If the maximum number of resources is already allocated, this will delay
92 : /// until one of them is released.
93 : Future<PoolResource> request() {
94 5 : if (isClosed) {
95 0 : throw new StateError("request() may not be called on a closed Pool.");
96 : }
97 :
98 15 : if (_allocatedResources < _maxAllocatedResources) {
99 10 : _allocatedResources++;
100 10 : return new Future.value(new PoolResource._(this));
101 0 : } else if (_onReleaseCallbacks.isNotEmpty) {
102 0 : return _runOnRelease(_onReleaseCallbacks.removeFirst());
103 : } else {
104 0 : var completer = new Completer<PoolResource>();
105 0 : _requestedResources.add(completer);
106 0 : _resetTimer();
107 0 : return completer.future;
108 : }
109 : }
110 :
111 : /// Requests a resource for the duration of [callback], which may return a
112 : /// Future.
113 : ///
114 : /// The return value of [callback] is piped to the returned Future.
115 : Future<T> withResource<T>(FutureOr<T> callback()) {
116 5 : if (isClosed) {
117 0 : throw new StateError(
118 : "withResource() may not be called on a closed Pool.");
119 : }
120 :
121 : // We can't use async/await here because we need to start the request
122 : // synchronously in case the pool is closed immediately afterwards. Async
123 : // functions have an asynchronous gap between calling and running the body,
124 : // and [close] could be called during that gap. See #3.
125 10 : return request().then<Future<T>>((resource) {
126 15 : return new Future<T>.sync(callback).whenComplete(resource.release);
127 : });
128 : }
129 :
130 : /// Closes the pool so that no more resources are requested.
131 : ///
132 : /// Existing resource requests remain unchanged.
133 : ///
134 : /// Any resources that are marked as releasable using
135 : /// [PoolResource.allowRelease] are released immediately. Once all resources
136 : /// have been released and any `onRelease` callbacks have completed, the
137 : /// returned future completes successfully. If any `onRelease` callback throws
138 : /// an error, the returned future completes with that error.
139 : ///
140 : /// This may be called more than once; it returns the same [Future] each time.
141 10 : Future close() => _closeMemo.runOnce(() {
142 5 : if (_closeGroup != null) return _closeGroup.future;
143 :
144 5 : _resetTimer();
145 :
146 10 : _closeGroup = new FutureGroup();
147 5 : for (var callback in _onReleaseCallbacks) {
148 0 : _closeGroup.add(new Future.sync(callback));
149 : }
150 :
151 20 : _allocatedResources -= _onReleaseCallbacks.length;
152 10 : _onReleaseCallbacks.clear();
153 :
154 10 : if (_allocatedResources == 0) _closeGroup.close();
155 10 : return _closeGroup.future;
156 : });
157 : final _closeMemo = new AsyncMemoizer();
158 :
159 : /// If there are any pending requests, this will fire the oldest one.
160 : void _onResourceReleased() {
161 5 : _resetTimer();
162 :
163 10 : if (_requestedResources.isNotEmpty) {
164 0 : var pending = _requestedResources.removeFirst();
165 0 : pending.complete(new PoolResource._(this));
166 : } else {
167 10 : _allocatedResources--;
168 5 : if (isClosed && _allocatedResources == 0) _closeGroup.close();
169 : }
170 : }
171 :
172 : /// If there are any pending requests, this will fire the oldest one after
173 : /// running [onRelease].
174 : void _onResourceReleaseAllowed(onRelease()) {
175 5 : _resetTimer();
176 :
177 10 : if (_requestedResources.isNotEmpty) {
178 0 : var pending = _requestedResources.removeFirst();
179 0 : pending.complete(_runOnRelease(onRelease));
180 5 : } else if (isClosed) {
181 15 : _closeGroup.add(new Future.sync(onRelease));
182 10 : _allocatedResources--;
183 20 : if (_allocatedResources == 0) _closeGroup.close();
184 : } else {
185 0 : var zone = Zone.current;
186 0 : var registered = zone.registerCallback(onRelease);
187 0 : _onReleaseCallbacks.add(() => zone.run(registered));
188 : }
189 : }
190 :
191 : /// Runs [onRelease] and returns a Future that completes to a resource once an
192 : /// [onRelease] callback completes.
193 : ///
194 : /// Futures returned by [_runOnRelease] always complete in the order they were
195 : /// created, even if earlier [onRelease] callbacks take longer to run.
196 : Future<PoolResource> _runOnRelease(onRelease()) {
197 0 : new Future.sync(onRelease).then((value) {
198 0 : _onReleaseCompleters.removeFirst().complete(new PoolResource._(this));
199 0 : }).catchError((error, stackTrace) {
200 0 : _onReleaseCompleters.removeFirst().completeError(error, stackTrace);
201 : });
202 :
203 0 : var completer = new Completer<PoolResource>.sync();
204 0 : _onReleaseCompleters.add(completer);
205 0 : return completer.future;
206 : }
207 :
208 : /// A resource has been requested, allocated, or released.
209 : void _resetTimer() {
210 5 : if (_timer == null) return;
211 :
212 0 : if (_requestedResources.isEmpty) {
213 0 : _timer.cancel();
214 : } else {
215 0 : _timer.reset();
216 : }
217 : }
218 :
219 : /// Handles [_timer] timing out by causing all pending resource completers to
220 : /// emit exceptions.
221 : void _onTimeout() {
222 0 : for (var completer in _requestedResources) {
223 0 : completer.completeError(
224 0 : new TimeoutException(
225 : "Pool deadlock: all resources have been "
226 : "allocated for too long.",
227 0 : _timeout),
228 0 : new Chain.current());
229 : }
230 0 : _requestedResources.clear();
231 0 : _timer = null;
232 : }
233 : }
234 :
235 : /// A member of a [Pool].
236 : ///
237 : /// A [PoolResource] is a token that indicates that a resource is allocated.
238 : /// When the associated resource is released, the user should call [release].
239 : class PoolResource {
240 : final Pool _pool;
241 :
242 : /// Whether [this] has been released yet.
243 : bool _released = false;
244 :
245 5 : PoolResource._(this._pool);
246 :
247 : /// Tells the parent [Pool] that the resource associated with this resource is
248 : /// no longer allocated, and that a new [PoolResource] may be allocated.
249 : void release() {
250 5 : if (_released) {
251 0 : throw new StateError("A PoolResource may only be released once.");
252 : }
253 5 : _released = true;
254 10 : _pool._onResourceReleased();
255 : }
256 :
257 : /// Tells the parent [Pool] that the resource associated with this resource is
258 : /// no longer necessary, but should remain allocated until more resources are
259 : /// needed.
260 : ///
261 : /// When [Pool.request] is called and there are no remaining available
262 : /// resources, the [onRelease] callback is called. It should free the
263 : /// resource, and it may return a Future or `null`. Once that completes, the
264 : /// [Pool.request] call will complete to a new [PoolResource].
265 : ///
266 : /// This is useful when a resource's main function is complete, but it may
267 : /// produce additional information later on. For example, an isolate's task
268 : /// may be complete, but it could still emit asynchronous errors.
269 : void allowRelease(onRelease()) {
270 5 : if (_released) {
271 0 : throw new StateError("A PoolResource may only be released once.");
272 : }
273 5 : _released = true;
274 10 : _pool._onResourceReleaseAllowed(onRelease);
275 : }
276 : }
|