| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2013, 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 library barback.file_pool; |
| 6 |
| 7 import 'dart:async'; |
| 8 import 'dart:collection'; |
| 9 import 'dart:convert'; |
| 10 import 'dart:io'; |
| 11 |
| 12 /// Manages a pool of files that are opened for reading to cope with maximum |
| 13 /// file descriptor limits. |
| 14 /// |
| 15 /// If a file cannot be opened because too many files are already open, this |
| 16 /// will defer the open until a previously opened file is closed and then try |
| 17 /// again. If this doesn't succeed after a certain amount of time, the open |
| 18 /// will fail and the original "too many files" exception will be thrown. |
| 19 class FilePool { |
| 20 /// [_FileReader]s whose last [listen] call failed and that are waiting for |
| 21 /// another file to close so they can be retried. |
| 22 final _pendingListens = new Queue<_FileReader>(); |
| 23 |
| 24 /// Opens [file] for reading. |
| 25 /// |
| 26 /// When the returned stream is listened to, if there are too many files |
| 27 /// open, this will wait for a previously opened file to be closed and then |
| 28 /// try again. |
| 29 Stream<List<int>> openRead(File file) => new _FileReader(this, file).stream; |
| 30 |
| 31 /// Reads [file] as a string using [encoding]. |
| 32 /// |
| 33 /// If there are too many files open and the read fails, this will wait for |
| 34 /// a previously opened file to be closed and then try again. |
| 35 Future<String> readAsString(File file, Encoding encoding) { |
| 36 return _readAsBytes(file).then(encoding.decode); |
| 37 } |
| 38 |
| 39 /// Reads [file] as a list of bytes, using [openRead] to retry if there are |
| 40 /// failures. |
| 41 Future<List<int>> _readAsBytes(File file) { |
| 42 var completer = new Completer<List<int>>(); |
| 43 var builder = new BytesBuilder(); |
| 44 |
| 45 openRead(file).listen(builder.add, onDone: () { |
| 46 completer.complete(builder.takeBytes()); |
| 47 }, onError: completer.completeError, cancelOnError: true); |
| 48 |
| 49 return completer.future; |
| 50 } |
| 51 |
| 52 /// Tries to re-listen to the next pending file reader if there are any. |
| 53 void _retryPendingListen() { |
| 54 if (_pendingListens.isEmpty) return; |
| 55 |
| 56 var pending = _pendingListens.removeFirst(); |
| 57 pending._listen(); |
| 58 } |
| 59 } |
| 60 |
| 61 /// Wraps a raw file reading stream in a stream that handles "too many files" |
| 62 /// errors. |
| 63 /// |
| 64 /// This also notifies the pool when the underlying file stream is closed so |
| 65 /// that it can try to open a waiting file. |
| 66 class _FileReader { |
| 67 final FilePool _pool; |
| 68 final File _file; |
| 69 |
| 70 /// The underyling file stream. |
| 71 Stream<List<int>> _fileStream; |
| 72 |
| 73 /// The controller for the stream wrapper. |
| 74 StreamController<List<int>> _controller; |
| 75 |
| 76 /// The current subscription to the underlying file stream. |
| 77 /// |
| 78 /// This will only be non-null while the wrapped stream is being listened to. |
| 79 StreamSubscription _subscription; |
| 80 |
| 81 /// The timeout timer. |
| 82 /// |
| 83 /// If this timer fires before the listen is retried, it gives up and throws |
| 84 /// the original error. |
| 85 Timer _timer; |
| 86 |
| 87 /// When a [listen] call has thrown a "too many files" error, this will be |
| 88 /// the exception object. |
| 89 Object _exception; |
| 90 |
| 91 /// When a [listen] call has thrown a "too many files" error, this will be |
| 92 /// the captured stack trace. |
| 93 Object _stackTrace; |
| 94 |
| 95 /// The wrapped stream that the file can be read from. |
| 96 Stream<List<int>> get stream => _controller.stream; |
| 97 |
| 98 _FileReader(this._pool, this._file) { |
| 99 _controller = new StreamController<List<int>>(onListen: _listen, |
| 100 onPause: () { |
| 101 _subscription.pause(); |
| 102 }, onResume: () { |
| 103 _subscription.resume(); |
| 104 }, onCancel: () { |
| 105 if (_subscription != null) _subscription.cancel(); |
| 106 _subscription = null; |
| 107 }, sync: true); |
| 108 } |
| 109 |
| 110 /// Starts listening to the underlying file stream. |
| 111 void _listen() { |
| 112 if (_timer != null) { |
| 113 _timer.cancel(); |
| 114 _timer = null; |
| 115 } |
| 116 |
| 117 _exception = null; |
| 118 _stackTrace = null; |
| 119 |
| 120 _fileStream = _file.openRead(); |
| 121 _subscription = _fileStream.listen(_controller.add, |
| 122 onError: _onError, onDone: _onDone, cancelOnError: true); |
| 123 } |
| 124 |
| 125 /// Handles an error from the underlying file stream. |
| 126 /// |
| 127 /// "Too many file" errors are caught so that we can retry later. Other |
| 128 /// errors are passed to the wrapped stream and the underlying stream |
| 129 /// subscription is canceled. |
| 130 void _onError(Object exception, Object stackTrace) { |
| 131 assert(_subscription != null); |
| 132 assert(_exception == null); |
| 133 |
| 134 // The subscription is canceled after an error. |
| 135 _subscription = null; |
| 136 |
| 137 // We only handle "Too many open files errors". |
| 138 if (exception is! FileException || exception.osError.errorCode != 24) { |
| 139 _controller.addError(exception, stackTrace); |
| 140 return; |
| 141 } |
| 142 |
| 143 _exception = exception; |
| 144 _stackTrace = stackTrace; |
| 145 |
| 146 // We'll try to defer the listen in the hopes that another file will close |
| 147 // and we can try. If that doesn't happen after a while, give up and just |
| 148 // throw the original error. |
| 149 // TODO(rnystrom): The point of this timer is to not get stuck forever in |
| 150 // a deadlock scenario. But this can also erroneously fire if there is a |
| 151 // large number of slow reads that do incrementally finish. A file may not |
| 152 // move to the front of the queue in time even though it is making |
| 153 // progress. A better solution is to have a single deadlock timer on the |
| 154 // FilePool itself that starts when a pending listen is enqueued and checks |
| 155 // to see if progress has been made when it fires. |
| 156 _timer = new Timer(new Duration(seconds: 60), _onTimeout); |
| 157 |
| 158 // Tell the pool that this file is waiting. |
| 159 _pool._pendingListens.add(this); |
| 160 } |
| 161 |
| 162 /// Handles the underlying file stream finishing. |
| 163 void _onDone() { |
| 164 _subscription = null; |
| 165 |
| 166 _controller.close(); |
| 167 _pool._retryPendingListen(); |
| 168 } |
| 169 |
| 170 /// If this file failed to be read because there were too many open files and |
| 171 /// no file was closed in time to retry, this handles giving up. |
| 172 void _onTimeout() { |
| 173 assert(_subscription == null); |
| 174 assert(_exception != null); |
| 175 |
| 176 // We failed to open in time, so just fail with the original error. |
| 177 _pool._pendingListens.remove(this); |
| 178 _controller.addError(_exception, _stackTrace); |
| 179 _controller.close(); |
| 180 |
| 181 _timer = null; |
| 182 _exception = null; |
| 183 _stackTrace = null; |
| 184 } |
| 185 } |
| OLD | NEW |