Skip to content

Commit 0a590cf

Browse files
committed
Fix stranded Scanning root
1 parent 2ac8b2f commit 0a590cf

1 file changed

Lines changed: 75 additions & 24 deletions

File tree

crates/oak_scan/src/scheduler.rs

Lines changed: 75 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,19 @@ impl ScanScheduler {
297297
Some(root) if self.state.contains_key(&root) => {
298298
self.buffered.entry(root).or_default().push(event);
299299
},
300-
_ => apply_one_event_surgically(db, event),
300+
// No in-flight scan for this root: apply the event directly,
301+
// the watcher's single-file fast path.
302+
_ => match event.kind {
303+
FileEventKind::Created | FileEventKind::Changed => {
304+
match std::fs::read_to_string(&path) {
305+
Ok(contents) => add_watched_file(db, event.url, contents),
306+
Err(err) => {
307+
log::warn!("Skipped watched file {}: {err:?}", path.display())
308+
},
309+
}
310+
},
311+
FileEventKind::Deleted => remove_watched_file(db, event.url),
312+
},
301313
}
302314
}
303315

@@ -339,21 +351,22 @@ impl ScanScheduler {
339351
let prior = self.state.remove(&root);
340352
match prior {
341353
Some(ScanState::ScanningWithRescanQueued) => {
342-
// A rescan was queued mid-scan. Spawn it and keep the
343-
// buffer; replay happens when this next scan finishes idle.
344-
self.state.insert(root, ScanState::Scanning);
345-
let Some(path) = root.path(db).to_file_path().warn_on_err() else {
346-
return Vec::new();
347-
};
348-
vec![ScanRequest { root, path }]
349-
},
350-
Some(ScanState::Scanning) => {
351-
// We're now idle. Drain any buffered events through the normal path.
352-
match self.buffered.remove(&root) {
353-
Some(buffered) => self.apply_watcher_events(db, buffered, editor_owned),
354-
None => Vec::new(),
354+
// A rescan was queued mid-scan. Resolve its path before
355+
// re-marking the root `Scanning`: a path we can't resolve must
356+
// not leave the root `Scanning` with no scan in flight, or it
357+
// would stay pending forever. On success the buffer rides along
358+
// and replays when the requeued scan finishes. On failure we
359+
// fall back to the idle drain.
360+
match root.path(db).to_file_path().warn_on_err() {
361+
Some(path) => {
362+
self.state.insert(root, ScanState::Scanning);
363+
vec![ScanRequest { root, path }]
364+
},
365+
None => self.drain_buffered(db, root, editor_owned),
355366
}
356367
},
368+
// We're now idle. Drain any buffered events through the normal path.
369+
Some(ScanState::Scanning) => self.drain_buffered(db, root, editor_owned),
357370
None => {
358371
// A completion for a root we weren't tracking as scanning.
359372
// Every dispatched scan marks its root `Scanning`, so reaching
@@ -369,6 +382,21 @@ impl ScanScheduler {
369382
}
370383
}
371384

385+
/// Replay the watcher events buffered for `root` while its scan was in
386+
/// flight, now that the root is idle. Routes them through
387+
/// [`Self::apply_watcher_events`], which may itself return fresh requests.
388+
fn drain_buffered<DB: Db + DbInputs>(
389+
&mut self,
390+
db: &mut DB,
391+
root: Root,
392+
editor_owned: &HashSet<UrlId>,
393+
) -> Vec<ScanRequest> {
394+
match self.buffered.remove(&root) {
395+
Some(buffered) => self.apply_watcher_events(db, buffered, editor_owned),
396+
None => Vec::new(),
397+
}
398+
}
399+
372400
fn request_rescan<DB: Db + DbInputs>(
373401
&mut self,
374402
db: &mut DB,
@@ -418,15 +446,38 @@ fn workspace_root_paths<DB: Db + DbInputs>(db: &DB) -> Vec<(PathBuf, Root)> {
418446
.collect()
419447
}
420448

421-
fn apply_one_event_surgically<DB: Db + DbInputs>(db: &mut DB, event: FileEvent) {
422-
let Ok(path) = event.url.to_file_path() else {
423-
return;
424-
};
425-
match event.kind {
426-
FileEventKind::Created | FileEventKind::Changed => match std::fs::read_to_string(&path) {
427-
Ok(contents) => add_watched_file(db, event.url, contents),
428-
Err(err) => log::warn!("Skipped watched file {}: {err:?}", path.display()),
429-
},
430-
FileEventKind::Deleted => remove_watched_file(db, event.url),
449+
#[cfg(test)]
450+
mod tests {
451+
use oak_db::OakDatabase;
452+
453+
use super::*;
454+
455+
/// A root whose URL has no filesystem path (e.g. an `untitled:` buffer)
456+
/// can't produce a `ScanRequest`. When such a root is
457+
/// `ScanningWithRescanQueued` and its scan completes, the scheduler must
458+
/// not leave it `Scanning` with nothing in flight, or it would stay pending
459+
/// forever. This state is unreachable through the public API (workspace
460+
/// roots always come from `from_file_path`), so we build it by hand.
461+
#[test]
462+
fn test_unresolvable_rescan_path_does_not_strand_root_scanning() {
463+
let mut db = OakDatabase::new();
464+
let url = UrlId::parse("untitled:Untitled-1").unwrap();
465+
let root = Root::new(&db, url, RootKind::Workspace, Vec::new(), Vec::new());
466+
db.workspace_roots().set_roots(&mut db).to(vec![root]);
467+
468+
let mut scheduler = ScanScheduler::new();
469+
scheduler
470+
.state
471+
.insert(root, ScanState::ScanningWithRescanQueued);
472+
473+
let result = ScanCompleted {
474+
root,
475+
packages: Vec::new(),
476+
scripts: Vec::new(),
477+
};
478+
let requests = scheduler.apply_scan_completed(&mut db, result, &HashSet::new());
479+
480+
assert!(requests.is_empty());
481+
assert!(!scheduler.has_pending_scans());
431482
}
432483
}

0 commit comments

Comments
 (0)