fix SchedulerService isolate-spawn race (T-106)

_startTicker fired Isolate.spawn(...).then((iso) => _isolate = iso)
and returned. If _stopTicker landed before the spawn future resolved,
_isolate was still null at kill time and the just-spawned isolate
(with its Timer.periodic) leaked forever.

Track the spawn as _isolateReady and have _stopTicker await it before
killing. Same shape as the NativePty fix from T-96.

dispose() is now async; the single caller in facade.dart already
sat inside an async dispose chain and just needed the await.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 21:19:20 +02:00
co-authored by Claude Opus 4.7
parent 06b08b7388
commit 2768f11767
6 changed files with 96 additions and 7 deletions
+17
View File
@@ -220,5 +220,22 @@ void main() {
bus.emit(const ProjectClosed());
// Smoke — no throw.
});
test('dispose immediately after ProjectOpened awaits the in-flight spawn (T-106)', () async {
// Race window: ProjectOpened triggers _startTicker which calls
// `Isolate.spawn(...)`; if dispose lands before the spawn future
// resolves, the old code left _isolate=null and the just-spawned
// ticker leaked forever. Now dispose awaits _isolateReady before
// killing.
final bus = DaemonBus();
final s = SchedulerService(bus);
s.start();
bus.emit(const ProjectOpened(path: '/tmp/x'));
// Don't wait for the spawn to settle — dispose must do that itself.
await s.dispose();
// No throw, no leaked isolate (test runner would flag a lingering
// isolate by failing to exit cleanly). Reaching this line is the
// assertion.
});
});
}