fix(sync): cancel backoff/idle timers on stop to prevent process hang

Future.any([Future.delayed(N), stopSignal.future]) left unfired Timers
alive after stop() fired the signal — pending Timers kept the Dart event
loop running and prevented the process from exiting, causing the E2E
integration test to time out (exit 124) instead of exiting cleanly.

Replace all four occurrences with an explicit Timer that completes the
stop-signal and is cancelled in a finally block, so the Dart isolate can
exit as soon as the sync loops are stopped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Thomas SharedInbox
2026-05-14 22:03:26 +02:00
co-authored by Claude Sonnet 4.6
parent 4b83d3e456
commit cc108b4788
+42 -20
View File
@@ -201,6 +201,7 @@ class _AccountSync implements _SyncLoop {
bool _running = false;
int _backoffSeconds = 5;
Completer<void>? _stopSignal;
Timer? _waitTimer;
@override
void start() {
@@ -303,11 +304,16 @@ class _AccountSync implements _SyncLoop {
Future<void> _waitSeconds(int seconds) async {
if (!_running) return;
_stopSignal = Completer<void>();
await Future.any([
Future.delayed(Duration(seconds: seconds)),
_stopSignal!.future,
]);
_stopSignal = null;
_waitTimer = Timer(Duration(seconds: seconds), () {
if (!_stopSignal!.isCompleted) _stopSignal!.complete();
});
try {
await _stopSignal!.future;
} finally {
_waitTimer?.cancel();
_waitTimer = null;
_stopSignal = null;
}
}
Future<(_SyncStats, String?)> _runSync(bool verbose) async {
@@ -394,11 +400,16 @@ class _AccountSync implements _SyncLoop {
// Cap IDLE at 25 minutes (RFC 2177). Also wakes up when stop() is
// called or a new message / expunge event arrives.
await Future.any([
newMessageCompleter.future,
Future.delayed(const Duration(minutes: 25)),
_stopSignal!.future,
]);
final idleTimer = Timer(const Duration(minutes: 25), () {
if (_stopSignal != null && !_stopSignal!.isCompleted) {
_stopSignal!.complete();
}
});
try {
await Future.any([newMessageCompleter.future, _stopSignal!.future]);
} finally {
idleTimer.cancel();
}
await client.idleDone();
await sub.cancel();
@@ -439,6 +450,7 @@ class _JmapAccountSync implements _SyncLoop {
bool _running = false;
int _backoffSeconds = 5;
Completer<void>? _stopSignal;
Timer? _waitTimer;
static const _pollInterval = Duration(seconds: 30);
@@ -542,11 +554,16 @@ class _JmapAccountSync implements _SyncLoop {
Future<void> _waitSeconds(int seconds) async {
if (!_running) return;
_stopSignal = Completer<void>();
await Future.any([
Future.delayed(Duration(seconds: seconds)),
_stopSignal!.future,
]);
_stopSignal = null;
_waitTimer = Timer(Duration(seconds: seconds), () {
if (!_stopSignal!.isCompleted) _stopSignal!.complete();
});
try {
await _stopSignal!.future;
} finally {
_waitTimer?.cancel();
_waitTimer = null;
_stopSignal = null;
}
}
Future<(_SyncStats, String?)> _runSync(bool verbose) async {
@@ -618,11 +635,16 @@ class _JmapAccountSync implements _SyncLoop {
onError: (_) {},
);
await Future.any([
pushReady.future,
Future.delayed(_pollInterval),
_stopSignal!.future,
]);
final pollTimer = Timer(_pollInterval, () {
if (_stopSignal != null && !_stopSignal!.isCompleted) {
_stopSignal!.complete();
}
});
try {
await Future.any([pushReady.future, _stopSignal!.future]);
} finally {
pollTimer.cancel();
}
await pushSub.cancel();
_stopSignal = null;