LCOV - code coverage report
Current view: top level - pool-1.5.0/lib - pool.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 0 115 0.0 %
Date: 2021-11-28 14:37:50 Functions: 0 0 -

          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             : }

Generated by: LCOV version 1.14