Skip to content

Commit cda9f27

Browse files
committed
Fix async lock with fake_async zones
1 parent 0901c98 commit cda9f27

File tree

4 files changed

+54
-4
lines changed

4 files changed

+54
-4
lines changed

drift/CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
SQLite databases.
55
- Don't attempt to roll-back transactions that failed to begin.
66
- Fix unhandled exception when cancelling transactions.
7+
- Fix deadlock when drift databases are used in a `fake_async` Zone and then
8+
closed outside that zone.
79

810
## 2.23.0
911

drift/lib/src/utils/synchronized.dart

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,32 @@ class Lock {
1111
// This completer may not be sync: It must complete just after
1212
// callBlockAndComplete completes.
1313
final blockCompleted = Completer<void>();
14-
_last = blockCompleted.future;
14+
final blockReleasedLock = blockCompleted.future;
15+
_last = blockReleasedLock;
1516

1617
Future<T> callBlockAndComplete() {
17-
return Future.sync(block).whenComplete(blockCompleted.complete);
18+
return Future.sync(block).whenComplete(() {
19+
blockCompleted.complete();
20+
21+
if (identical(_last, blockReleasedLock)) {
22+
// There's no subsequent waiter entering the lock now, so we can reset
23+
// the entire state.
24+
_last = null;
25+
26+
// This doesn't affect the correctness of the lock, but is helpful
27+
// when drift is used in `fake_async` scenarios but then cleaned up
28+
// outside of that `fake_async` scope (a very common pattern in
29+
// Flutter widget tests).
30+
// Waiting on `previous.then` on a completed `previous` future will
31+
// schedule a microtask, so if we call synchronized in a zone outside
32+
// of fake_async and the lock was previously locked in a fake_async
33+
// zone, that microtask might not run if no one completes the pending
34+
// fake_async microtasks.
35+
// Since the lock is idle anyway, the next waiter can just call
36+
// callBlockAndComplete() directly without calling `.then()` on a
37+
// future that will no longer notify listeners.
38+
}
39+
});
1840
}
1941

2042
if (previous != null) {

drift/pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,4 @@ dev_dependencies:
4848
vm_service: ^15.0.0
4949
rxdart: ^0.28.0
5050
build_daemon: ^4.0.3
51+
fake_async: ^1.3.0
Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,43 @@
1+
import 'dart:async';
2+
13
import 'package:drift/src/utils/synchronized.dart';
4+
import 'package:fake_async/fake_async.dart';
25
import 'package:test/test.dart';
36

47
void main() {
58
test('synchronized runs code in sequence', () async {
69
final lock = Lock();
710
var i = 0;
11+
var inSynchronizedBlock = 0;
812
final completionOrder = <int>[];
913
final futures = List.generate(
1014
100,
11-
(index) => lock.synchronized(() => i++)
12-
..whenComplete(() => completionOrder.add(index)));
15+
(index) => lock.synchronized(() async {
16+
expect(inSynchronizedBlock, 0);
17+
inSynchronizedBlock = 1;
18+
await pumpEventQueue();
19+
inSynchronizedBlock--;
20+
return i++;
21+
})
22+
..whenComplete(() => completionOrder.add(index)));
1323
final results = await Future.wait(futures);
1424

1525
expect(results, List.generate(100, (index) => index));
1626
expect(completionOrder, List.generate(100, (index) => index));
1727
});
28+
29+
test('can wait on lock used in fakeAsync zone', () async {
30+
final lock = Lock();
31+
final completer = Completer<void>();
32+
33+
fakeAsync((async) {
34+
lock
35+
.synchronized(expectAsync0(() async {}))
36+
.then((_) => completer.complete());
37+
async.flushTimers();
38+
});
39+
40+
await completer.future;
41+
await lock.synchronized(() async {});
42+
});
1843
}

0 commit comments

Comments
 (0)