Skip to content

Commit b110f0e

Browse files
committed
Fix ordering of debounced events when multiple files are modified and renamed
Closes #587
1 parent 4dbe5b3 commit b110f0e

9 files changed

+327
-53
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ v4 commits split out to branch `v4_maintenance` starting with `4.0.16`
2727
- CHANGE: add `RecommendedCache`, which automatically enables the file ID cache on Windows and MacOS
2828
and disables it on Linux, where it is not needed
2929

30+
- FIX: ordering of debounced events when multiple files are modified and renamed (eg. during a safe save performed by Blender)
31+
3032
## debouncer-full 0.3.1 (2023-08-21)
3133

3234
- CHANGE: remove serde binary experiment opt-out after it got removed [#530]

notify-debouncer-full/src/lib.rs

Lines changed: 130 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ struct Queue {
152152

153153
impl Queue {
154154
fn was_created(&self) -> bool {
155-
self.events.front().map_or(false, |event| {
155+
self.events.front().is_some_and(|event| {
156156
matches!(
157157
event.kind,
158158
EventKind::Create(_) | EventKind::Modify(ModifyKind::Name(RenameMode::To))
@@ -161,7 +161,7 @@ impl Queue {
161161
}
162162

163163
fn was_removed(&self) -> bool {
164-
self.events.front().map_or(false, |event| {
164+
self.events.front().is_some_and(|event| {
165165
matches!(
166166
event.kind,
167167
EventKind::Remove(_) | EventKind::Modify(ModifyKind::Name(RenameMode::From))
@@ -170,9 +170,48 @@ impl Queue {
170170
}
171171
}
172172

173+
#[derive(Debug)]
174+
pub struct BlockEntry {
175+
pub blocker_path: PathBuf,
176+
pub blocker_time: Instant,
177+
pub blockee_path: PathBuf,
178+
}
179+
180+
#[derive(Debug, Default)]
181+
pub struct BlockManager {
182+
entries: Vec<BlockEntry>,
183+
}
184+
185+
impl BlockManager {
186+
pub fn new() -> BlockManager {
187+
BlockManager {
188+
entries: Vec::new(),
189+
}
190+
}
191+
192+
pub fn add_blocker(&mut self, entry: BlockEntry) {
193+
self.entries.push(entry);
194+
}
195+
196+
pub fn remove_blocker(&mut self, path: &PathBuf, time: Instant) {
197+
self.entries
198+
.retain(|entry| entry.blocker_path != *path || entry.blocker_time != time);
199+
}
200+
201+
pub fn is_blocked_by(&self, path: &PathBuf) -> Option<(&PathBuf, Instant)> {
202+
for entry in &self.entries {
203+
if entry.blockee_path == *path {
204+
return Some((&entry.blocker_path, entry.blocker_time));
205+
}
206+
}
207+
None
208+
}
209+
}
210+
173211
#[derive(Debug)]
174212
pub(crate) struct DebounceDataInner<T> {
175213
queues: HashMap<PathBuf, Queue>,
214+
blocking: BlockManager,
176215
roots: Vec<(PathBuf, RecursiveMode)>,
177216
cache: T,
178217
rename_event: Option<(DebouncedEvent, Option<FileId>)>,
@@ -185,6 +224,7 @@ impl<T: FileIdCache> DebounceDataInner<T> {
185224
pub(crate) fn new(cache: T, timeout: Duration) -> Self {
186225
Self {
187226
queues: HashMap::new(),
227+
blocking: BlockManager::new(),
188228
roots: Vec::new(),
189229
cache,
190230
rename_event: None,
@@ -194,11 +234,17 @@ impl<T: FileIdCache> DebounceDataInner<T> {
194234
}
195235
}
196236

237+
fn contains_event(&self, path: &PathBuf, time: Instant) -> bool {
238+
self.queues
239+
.get(path)
240+
.is_some_and(|queue| queue.events.iter().any(|event| event.time == time))
241+
}
242+
197243
/// Retrieve a vec of debounced events, removing them if not continuous
198244
pub fn debounced_events(&mut self) -> Vec<DebouncedEvent> {
199245
let now = Instant::now();
200-
let mut events_expired = Vec::with_capacity(self.queues.len());
201-
let mut queues_remaining = HashMap::with_capacity(self.queues.len());
246+
let events_count = self.queues.values().map(|queue| queue.events.len()).sum();
247+
let mut events_expired = Vec::with_capacity(events_count);
202248

203249
if let Some(event) = self.rescan_event.take() {
204250
if now.saturating_duration_since(event.time) >= self.timeout {
@@ -209,48 +255,62 @@ impl<T: FileIdCache> DebounceDataInner<T> {
209255
}
210256
}
211257

212-
// TODO: perfect fit for drain_filter https://github.com/rust-lang/rust/issues/59618
213-
for (path, mut queue) in self.queues.drain() {
214-
let mut kind_index = HashMap::new();
215-
216-
while let Some(event) = queue.events.pop_front() {
217-
if now.saturating_duration_since(event.time) >= self.timeout {
218-
// remove previous event of the same kind
219-
if let Some(idx) = kind_index.get(&event.kind).copied() {
220-
events_expired.remove(idx);
221-
222-
kind_index.values_mut().for_each(|i| {
223-
if *i > idx {
224-
*i -= 1
225-
}
226-
})
227-
}
258+
let mut kind_index: HashMap<PathBuf, HashMap<EventKind, usize>> = HashMap::new();
228259

229-
kind_index.insert(event.kind, events_expired.len());
260+
while let Some(path) = self
261+
.queues
262+
// iterate over all queues
263+
.iter()
264+
// get the first event of every queue
265+
.filter_map(|(path, queue)| queue.events.front().map(|event| (path, event.time)))
266+
// filter out all blocked events
267+
.filter(|(path, _)| {
268+
self.blocking
269+
.is_blocked_by(path)
270+
.map_or(true, |(path, time)| !self.contains_event(path, time))
271+
})
272+
// get the event with the earliest timestamp
273+
.min_by_key(|(_, time)| *time)
274+
// get the path of the event
275+
.map(|(path, _)| path.clone())
276+
{
277+
let event = self
278+
.queues
279+
.get_mut(&path)
280+
.unwrap()
281+
.events
282+
.pop_front()
283+
.unwrap();
230284

231-
events_expired.push(event);
232-
} else {
233-
queue.events.push_front(event);
234-
break;
285+
if now.saturating_duration_since(event.time) >= self.timeout {
286+
// remove previous event of the same kind
287+
let kind_index = kind_index.entry(path.clone()).or_default();
288+
if let Some(idx) = kind_index.get(&event.kind).copied() {
289+
events_expired.remove(idx);
290+
291+
kind_index.values_mut().for_each(|i| {
292+
if *i > idx {
293+
*i -= 1
294+
}
295+
})
235296
}
236-
}
297+
kind_index.insert(event.kind, events_expired.len());
237298

238-
if !queue.events.is_empty() {
239-
queues_remaining.insert(path, queue);
299+
self.blocking.remove_blocker(&path, event.time);
300+
301+
events_expired.push(event);
302+
} else {
303+
self.queues.get_mut(&path).unwrap().events.push_front(event);
304+
305+
break;
240306
}
241307
}
242308

243-
self.queues = queues_remaining;
309+
self.queues.retain(|_, queue| !queue.events.is_empty());
244310

245-
// order events for different files chronologically, but keep the order of events for the same file
246-
events_expired.sort_by(|event_a, event_b| {
247-
// use the last path because rename events are emitted for the target path
248-
if event_a.paths.last() == event_b.paths.last() {
249-
std::cmp::Ordering::Equal
250-
} else {
251-
event_a.time.cmp(&event_b.time)
252-
}
253-
});
311+
if self.queues.is_empty() {
312+
self.blocking.entries.clear();
313+
}
254314

255315
events_expired
256316
}
@@ -425,18 +485,6 @@ impl<T: FileIdCache> DebounceDataInner<T> {
425485
source_queue.events.remove(remove_index);
426486
}
427487

428-
// split off remove or move out event and add it back to the events map
429-
if source_queue.was_removed() {
430-
let event = source_queue.events.pop_front().unwrap();
431-
432-
self.queues.insert(
433-
event.paths[0].clone(),
434-
Queue {
435-
events: [event].into(),
436-
},
437-
);
438-
}
439-
440488
// update paths
441489
for e in &mut source_queue.events {
442490
e.paths = vec![event.paths[0].clone()];
@@ -455,7 +503,12 @@ impl<T: FileIdCache> DebounceDataInner<T> {
455503
}
456504

457505
if let Some(target_queue) = self.queues.get_mut(&event.paths[0]) {
458-
if !target_queue.was_created() {
506+
if target_queue.was_removed() {
507+
let event = target_queue.events.pop_front().unwrap();
508+
source_queue.events.push_front(event);
509+
}
510+
511+
if !target_queue.was_created() && !source_queue.was_removed() {
459512
let mut remove_event = DebouncedEvent {
460513
event: Event {
461514
kind: EventKind::Remove(RemoveKind::Any),
@@ -473,6 +526,8 @@ impl<T: FileIdCache> DebounceDataInner<T> {
473526
} else {
474527
self.queues.insert(event.paths[0].clone(), source_queue);
475528
}
529+
530+
self.find_blocked_events(&event.paths[0]);
476531
}
477532

478533
fn push_remove_event(&mut self, event: Event, time: Instant) {
@@ -518,6 +573,25 @@ impl<T: FileIdCache> DebounceDataInner<T> {
518573
);
519574
}
520575
}
576+
577+
fn find_blocked_events(&mut self, path: &Path) {
578+
for queue in self.queues.values_mut() {
579+
for event in &mut queue.events {
580+
if matches!(
581+
event.event.kind,
582+
EventKind::Modify(ModifyKind::Name(RenameMode::Both))
583+
) && event.event.paths[0] == path
584+
{
585+
self.blocking.add_blocker(BlockEntry {
586+
blocker_path: event.event.paths[1].clone(),
587+
blocker_time: event.time,
588+
blockee_path: path.to_path_buf(),
589+
});
590+
break;
591+
}
592+
}
593+
}
594+
}
521595
}
522596

523597
/// Debouncer guard, stops the debouncer on drop.
@@ -755,6 +829,11 @@ mod tests {
755829
"add_remove_parent_event_after_remove_child_event",
756830
"add_errors",
757831
"emit_continuous_modify_content_events",
832+
"emit_create_event_after_safe_save_and_backup_override",
833+
"emit_create_event_after_safe_save_and_backup_rotation_twice",
834+
"emit_create_event_after_safe_save_and_backup_rotation",
835+
"emit_create_event_after_safe_save_and_double_move",
836+
"emit_create_event_after_safe_save_and_double_move_and_recreate",
758837
"emit_events_in_chronological_order",
759838
"emit_events_with_a_prepended_rename_event",
760839
"emit_close_events_only_once",

notify-debouncer-full/src/testing.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use notify::{
1414
Error, ErrorKind, Event, EventKind, RecursiveMode,
1515
};
1616

17-
use crate::{DebounceDataInner, DebouncedEvent, FileIdCache, Queue};
17+
use crate::{BlockManager, DebounceDataInner, DebouncedEvent, FileIdCache, Queue};
1818

1919
pub(crate) use schema::TestCase;
2020

@@ -268,6 +268,7 @@ impl schema::State {
268268

269269
DebounceDataInner {
270270
queues,
271+
blocking: BlockManager::new(),
271272
roots: Vec::new(),
272273
cache,
273274
rename_event,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Based on the Blender safe save scenario.
2+
//
3+
// In this scenario the backup file is not removed first.
4+
{
5+
state: {}
6+
events: [
7+
{ kind: "create-any", paths: ["/watch/file@"], time: 1 }
8+
{ kind: "rename-from", paths: ["/watch/file"], tracker: 1, time: 3 }
9+
{ kind: "rename-to", paths: ["/watch/file1"], tracker: 1, time: 4 }
10+
{ kind: "rename-from", paths: ["/watch/file@"], tracker: 2, time: 5 }
11+
{ kind: "rename-to", paths: ["/watch/file"], tracker: 2, time: 6 }
12+
]
13+
expected: {
14+
queues: {
15+
/watch/file: {
16+
events: [
17+
{ kind: "create-any", paths: ["*"], time: 1 }
18+
]
19+
}
20+
/watch/file1: {
21+
events: [
22+
{ kind: "rename-both", paths: ["/watch/file", "/watch/file1"], tracker: 1, time: 3 }
23+
]
24+
}
25+
}
26+
events: {
27+
long: [
28+
{ kind: "rename-both", paths: ["/watch/file", "/watch/file1"], tracker: 1, time: 3 }
29+
{ kind: "create-any", paths: ["/watch/file"], time: 1 }
30+
]
31+
}
32+
}
33+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// https://github.com/notify-rs/notify/issues/587
2+
//
3+
// Blender causes the following events when saving a file:
4+
//
5+
// create test.blend@ (new content)
6+
// delete test.blend1 (delete backup)
7+
// rename test.blend -> test.blend1 (move current to backup)
8+
// rename test.blend@ -> test.blend (move new to current)
9+
{
10+
state: {}
11+
events: [
12+
{ kind: "create-any", paths: ["/watch/file@"], time: 1 }
13+
{ kind: "remove-any", paths: ["/watch/file1"], time: 2 }
14+
{ kind: "rename-from", paths: ["/watch/file"], tracker: 1, time: 3 }
15+
{ kind: "rename-to", paths: ["/watch/file1"], tracker: 1, time: 4 }
16+
{ kind: "rename-from", paths: ["/watch/file@"], tracker: 2, time: 5 }
17+
{ kind: "rename-to", paths: ["/watch/file"], tracker: 2, time: 6 }
18+
]
19+
expected: {
20+
queues: {
21+
/watch/file: {
22+
events: [
23+
{ kind: "create-any", paths: ["*"], time: 1 }
24+
]
25+
}
26+
/watch/file1: {
27+
events: [
28+
{ kind: "remove-any", paths: ["*"], time: 2 }
29+
{ kind: "rename-both", paths: ["/watch/file", "/watch/file1"], tracker: 1, time: 3 }
30+
]
31+
}
32+
}
33+
events: {
34+
long: [
35+
{ kind: "remove-any", paths: ["/watch/file1"], time: 2 }
36+
{ kind: "rename-both", paths: ["/watch/file", "/watch/file1"], tracker: 1, time: 3 }
37+
{ kind: "create-any", paths: ["/watch/file"], time: 1 }
38+
]
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)