Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
## 0.3.0 (unreleased)
## 0.3.1

### Fixes

* Fixed a download whose write to the cache file failed (a full disk, say) requesting the source again at once, again and again, reporting an error each time. The next attempt now waits 5 s, doubling to a minute while attempts keep failing.

## 0.3.0

### New Features

Expand Down
22 changes: 22 additions & 0 deletions lib/src/cache_stream/http_cache_stream.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:async';
import 'dart:io';
import 'dart:math' show min;

import 'package:http_cache_stream/src/cache_stream/cache_downloader/cache_downloader.dart';
import 'package:http_cache_stream/src/etc/extensions/future_extensions.dart';
Expand Down Expand Up @@ -206,6 +207,8 @@ class HttpCacheStream {
await _ensureInit();
_checkDisposed();

///Attempts in a row that ended on an error, see [_retryDelay].
int failedAttempts = 0;
while (true) {
final state = await refreshCacheState();
if (state.isComplete) {
Expand All @@ -223,11 +226,14 @@ class HttpCacheStream {
continue;
}

///The error this attempt ended on: one reported after the download last moved on.
Object? endError;
try {
final downloader =
_cacheDownloader = CacheDownloader.construct(metadata, config);
await downloader.download(
onPosition: (position) {
endError = null; //It moved on, past any error before
_updateCacheState(
CacheState.incomplete(position, downloader.sourceLength));
while (_queuedRequests.isNotEmpty &&
Expand All @@ -251,6 +257,7 @@ class HttpCacheStream {
},
onError: (e) {
assert(e is! InvalidCacheException);
endError = e;
_addError(e, closeRequests: true);
},
);
Expand All @@ -263,13 +270,28 @@ class HttpCacheStream {
}
if (!isRetained) rethrow;
await Future.delayed(const Duration(seconds: 5));
continue;
} finally {
_cacheDownloader = null;
}

if (endError == null) {
failedAttempts = 0;
} else if (isRetained) {
await Future.delayed(_retryDelay(++failedAttempts));
}
}
});
}

///How long [download] waits after its [attempts]th attempt in a row that ended on an error: 5 s,
///doubling to a minute. The downloader retries what the network does itself, after 5 s; an
///attempt ends on an error when it gives up, as a write to the cache file that fails (a full
///disk) gives it up, and without the wait the next attempt would request the source again at
///once, and fail again.
static Duration _retryDelay(final int attempts) =>
Duration(seconds: min(5 << min(attempts - 1, 4), 60));

Future<CachedResponseHeaders> downloadHeaders({bool save = true}) {
return _downloadHeadersFuture.run(() async {
final latestHeaders = await CachedResponseHeaders.fromUrl(
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: http_cache_stream
description: "Simultaneously download, cache, and stream remote content. Perfect for media players and any plugin that streams web content."
version: 0.3.0
version: 0.3.1
homepage: https://github.com/Colton127/http_cache_stream
repository: https://github.com/Colton127/http_cache_stream
topics:
Expand Down
58 changes: 58 additions & 0 deletions test/e2e/write_failure_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import 'dart:async';
import 'dart:io';

import 'package:flutter_test/flutter_test.dart';

import '../support/harness.dart';

/// A download whose writes to the cache file fail (a full disk, here a
/// directory where the file goes) gives up its attempt. The next one waits,
/// rather than requesting the source again at once, again and again.
void main() {
late CacheTestHarness h;

setUp(() async {
h = CacheTestHarness();
await h.setUp();
});

tearDown(() => h.tearDown());

test(
'an attempt whose cache writes fail is retried after a wait, not at once',
() async {
final stream = h.manager.createStream(h.origin.url('/a.mp3'));
await Directory(stream.files.partial.path).create(recursive: true);
final errors = <Object>[];
final subscription =
stream.cacheStateStream.listen(null, onError: errors.add);

stream.download().ignore();
await Future<void>.delayed(const Duration(seconds: 3));

expect(errors, isNotEmpty, reason: 'the failed write is reported');
expect(h.origin.requestCount, 1, reason: 'the next attempt waits 5 s');
await subscription.cancel();
await stream.dispose(force: true);
});

test('the attempt after the wait downloads once the cache can be written',
() async {
final stream = h.manager.createStream(h.origin.url('/a.mp3'));
final blocker = Directory(stream.files.partial.path);
await blocker.create(recursive: true);
final failed = Completer<void>();
final subscription = stream.cacheStateStream.listen(null, onError: (_) {
if (!failed.isCompleted) failed.complete();
});
final download = stream.download();
await failed.future.timeout(const Duration(seconds: 5));
await blocker.delete();

final file = await download.timeout(const Duration(seconds: 15));
expect(await file.length(), h.origin.payload.length);
expect(h.origin.requestCount, 2);
await subscription.cancel();
await stream.dispose(force: true);
});
}
Loading