LCOV - code coverage report
Current view: top level - pool-1.3.3/lib - pool.dart (source / functions) Hit Total Coverage
Test: coverage.lcov Lines: 37 73 50.7 %
Date: 2017-10-10 20:17:03 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 = 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             : }

Generated by: LCOV version 1.13