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 = 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 = Queue<void 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 = 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 0 : 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 0 : 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 0 : Pool(this._maxAllocatedResources, {Duration? timeout}) : _timeout = timeout {
82 0 : if (_maxAllocatedResources <= 0) {
83 0 : throw ArgumentError.value(_maxAllocatedResources, 'maxAllocatedResources',
84 : 'Must be greater than zero.');
85 : }
86 :
87 : if (timeout != null) {
88 : // Start the timer canceled since we only want to start counting down once
89 : // we've run out of available resources.
90 0 : _timer = RestartableTimer(timeout, _onTimeout)..cancel();
91 : }
92 : }
93 :
94 : /// Request a [PoolResource].
95 : ///
96 : /// If the maximum number of resources is already allocated, this will delay
97 : /// until one of them is released.
98 0 : Future<PoolResource> request() {
99 0 : if (isClosed) {
100 0 : throw StateError('request() may not be called on a closed Pool.');
101 : }
102 :
103 0 : if (_allocatedResources < _maxAllocatedResources) {
104 0 : _allocatedResources++;
105 0 : return Future.value(PoolResource._(this));
106 0 : } else if (_onReleaseCallbacks.isNotEmpty) {
107 0 : return _runOnRelease(_onReleaseCallbacks.removeFirst());
108 : } else {
109 0 : var completer = Completer<PoolResource>();
110 0 : _requestedResources.add(completer);
111 0 : _resetTimer();
112 0 : return completer.future;
113 : }
114 : }
115 :
116 : /// Requests a resource for the duration of [callback], which may return a
117 : /// Future.
118 : ///
119 : /// The return value of [callback] is piped to the returned Future.
120 0 : Future<T> withResource<T>(FutureOr<T> Function() callback) async {
121 0 : if (isClosed) {
122 0 : throw StateError('withResource() may not be called on a closed Pool.');
123 : }
124 :
125 0 : var resource = await request();
126 : try {
127 0 : return await callback();
128 : } finally {
129 0 : resource.release();
130 : }
131 : }
132 :
133 : /// Returns a [Stream] containing the result of [action] applied to each
134 : /// element of [elements].
135 : ///
136 : /// While [action] is invoked on each element of [elements] in order,
137 : /// it's possible the return [Stream] may have items out-of-order – especially
138 : /// if the completion time of [action] varies.
139 : ///
140 : /// If [action] throws an error the source item along with the error object
141 : /// and [StackTrace] are passed to [onError], if it is provided. If [onError]
142 : /// returns `true`, the error is added to the returned [Stream], otherwise
143 : /// it is ignored.
144 : ///
145 : /// Errors thrown from iterating [elements] will not be passed to
146 : /// [onError]. They will always be added to the returned stream as an error.
147 : ///
148 : /// Note: all of the resources of the this [Pool] will be used when the
149 : /// returned [Stream] is listened to until it is completed or canceled.
150 : ///
151 : /// Note: if this [Pool] is closed before the returned [Stream] is listened
152 : /// to, a [StateError] is thrown.
153 0 : Stream<T> forEach<S, T>(
154 : Iterable<S> elements, FutureOr<T> Function(S source) action,
155 : {bool Function(S item, Object error, StackTrace stack)? onError}) {
156 0 : onError ??= (item, e, s) => true;
157 :
158 : var cancelPending = false;
159 :
160 : Completer? resumeCompleter;
161 : late StreamController<T> controller;
162 :
163 : late Iterator<S> iterator;
164 :
165 0 : Future<void> run(int _) async {
166 0 : while (iterator.moveNext()) {
167 : // caching `current` is necessary because there are async breaks
168 : // in this code and `iterator` is shared across many workers
169 0 : final current = iterator.current;
170 :
171 0 : _resetTimer();
172 :
173 : if (resumeCompleter != null) {
174 0 : await resumeCompleter!.future;
175 : }
176 :
177 : if (cancelPending) {
178 : break;
179 : }
180 :
181 : T value;
182 : try {
183 0 : value = await action(current);
184 : } catch (e, stack) {
185 : if (onError!(current, e, stack)) {
186 0 : controller.addError(e, stack);
187 : }
188 : continue;
189 : }
190 0 : controller.add(value);
191 : }
192 : }
193 :
194 : Future<void>? doneFuture;
195 :
196 0 : void onListen() {
197 0 : iterator = elements.iterator;
198 :
199 0 : assert(doneFuture == null);
200 0 : var futures = Iterable<Future<void>>.generate(
201 0 : _maxAllocatedResources, (i) => withResource(() => run(i)));
202 0 : doneFuture = Future.wait(futures, eagerError: true)
203 0 : .then<void>((_) {})
204 0 : .catchError(controller.addError);
205 :
206 0 : doneFuture!.whenComplete(controller.close);
207 : }
208 :
209 0 : controller = StreamController<T>(
210 : sync: true,
211 : onListen: onListen,
212 0 : onCancel: () async {
213 0 : assert(!cancelPending);
214 : cancelPending = true;
215 0 : await doneFuture;
216 : },
217 0 : onPause: () {
218 0 : assert(resumeCompleter == null);
219 0 : resumeCompleter = Completer<void>();
220 : },
221 0 : onResume: () {
222 0 : assert(resumeCompleter != null);
223 0 : resumeCompleter!.complete();
224 : resumeCompleter = null;
225 : },
226 : );
227 :
228 0 : return controller.stream;
229 : }
230 :
231 : /// Closes the pool so that no more resources are requested.
232 : ///
233 : /// Existing resource requests remain unchanged.
234 : ///
235 : /// Any resources that are marked as releasable using
236 : /// [PoolResource.allowRelease] are released immediately. Once all resources
237 : /// have been released and any `onRelease` callbacks have completed, the
238 : /// returned future completes successfully. If any `onRelease` callback throws
239 : /// an error, the returned future completes with that error.
240 : ///
241 : /// This may be called more than once; it returns the same [Future] each time.
242 0 : Future close() => _closeMemo.runOnce(() {
243 0 : if (_closeGroup != null) return _closeGroup!.future;
244 :
245 0 : _resetTimer();
246 :
247 0 : _closeGroup = FutureGroup();
248 0 : for (var callback in _onReleaseCallbacks) {
249 0 : _closeGroup!.add(Future.sync(callback));
250 : }
251 :
252 0 : _allocatedResources -= _onReleaseCallbacks.length;
253 0 : _onReleaseCallbacks.clear();
254 :
255 0 : if (_allocatedResources == 0) _closeGroup!.close();
256 0 : return _closeGroup!.future;
257 : });
258 : final _closeMemo = AsyncMemoizer();
259 :
260 : /// If there are any pending requests, this will fire the oldest one.
261 0 : void _onResourceReleased() {
262 0 : _resetTimer();
263 :
264 0 : if (_requestedResources.isNotEmpty) {
265 0 : var pending = _requestedResources.removeFirst();
266 0 : pending.complete(PoolResource._(this));
267 : } else {
268 0 : _allocatedResources--;
269 0 : if (isClosed && _allocatedResources == 0) _closeGroup!.close();
270 : }
271 : }
272 :
273 : /// If there are any pending requests, this will fire the oldest one after
274 : /// running [onRelease].
275 0 : void _onResourceReleaseAllowed(Function() onRelease) {
276 0 : _resetTimer();
277 :
278 0 : if (_requestedResources.isNotEmpty) {
279 0 : var pending = _requestedResources.removeFirst();
280 0 : pending.complete(_runOnRelease(onRelease));
281 0 : } else if (isClosed) {
282 0 : _closeGroup!.add(Future.sync(onRelease));
283 0 : _allocatedResources--;
284 0 : if (_allocatedResources == 0) _closeGroup!.close();
285 : } else {
286 0 : var zone = Zone.current;
287 0 : var registered = zone.registerCallback(onRelease);
288 0 : _onReleaseCallbacks.add(() => zone.run(registered));
289 : }
290 : }
291 :
292 : /// Runs [onRelease] and returns a Future that completes to a resource once an
293 : /// [onRelease] callback completes.
294 : ///
295 : /// Futures returned by [_runOnRelease] always complete in the order they were
296 : /// created, even if earlier [onRelease] callbacks take longer to run.
297 0 : Future<PoolResource> _runOnRelease(Function() onRelease) {
298 0 : Future.sync(onRelease).then((value) {
299 0 : _onReleaseCompleters.removeFirst().complete(PoolResource._(this));
300 0 : }).catchError((Object error, StackTrace stackTrace) {
301 0 : _onReleaseCompleters.removeFirst().completeError(error, stackTrace);
302 : });
303 :
304 0 : var completer = Completer<PoolResource>.sync();
305 0 : _onReleaseCompleters.add(completer);
306 0 : return completer.future;
307 : }
308 :
309 : /// A resource has been requested, allocated, or released.
310 0 : void _resetTimer() {
311 0 : if (_timer == null) return;
312 :
313 0 : if (_requestedResources.isEmpty) {
314 0 : _timer!.cancel();
315 : } else {
316 0 : _timer!.reset();
317 : }
318 : }
319 :
320 : /// Handles [_timer] timing out by causing all pending resource completers to
321 : /// emit exceptions.
322 0 : void _onTimeout() {
323 0 : for (var completer in _requestedResources) {
324 0 : completer.completeError(
325 0 : TimeoutException(
326 : 'Pool deadlock: all resources have been '
327 : 'allocated for too long.',
328 0 : _timeout),
329 0 : Chain.current());
330 : }
331 0 : _requestedResources.clear();
332 0 : _timer = null;
333 : }
334 : }
335 :
336 : /// A member of a [Pool].
337 : ///
338 : /// A [PoolResource] is a token that indicates that a resource is allocated.
339 : /// When the associated resource is released, the user should call [release].
340 : class PoolResource {
341 : final Pool _pool;
342 :
343 : /// Whether `this` has been released yet.
344 : bool _released = false;
345 :
346 0 : PoolResource._(this._pool);
347 :
348 : /// Tells the parent [Pool] that the resource associated with this resource is
349 : /// no longer allocated, and that a new [PoolResource] may be allocated.
350 0 : void release() {
351 0 : if (_released) {
352 0 : throw StateError('A PoolResource may only be released once.');
353 : }
354 0 : _released = true;
355 0 : _pool._onResourceReleased();
356 : }
357 :
358 : /// Tells the parent [Pool] that the resource associated with this resource is
359 : /// no longer necessary, but should remain allocated until more resources are
360 : /// needed.
361 : ///
362 : /// When [Pool.request] is called and there are no remaining available
363 : /// resources, the [onRelease] callback is called. It should free the
364 : /// resource, and it may return a Future or `null`. Once that completes, the
365 : /// [Pool.request] call will complete to a new [PoolResource].
366 : ///
367 : /// This is useful when a resource's main function is complete, but it may
368 : /// produce additional information later on. For example, an isolate's task
369 : /// may be complete, but it could still emit asynchronous errors.
370 0 : void allowRelease(Function() onRelease) {
371 0 : if (_released) {
372 0 : throw StateError('A PoolResource may only be released once.');
373 : }
374 0 : _released = true;
375 0 : _pool._onResourceReleaseAllowed(onRelease);
376 : }
377 : }
|