Skip to content

Commit 57a24ed

Browse files
committed
Fix ordering of debounced events when multiple files are modified and renamed
Closes #587
1 parent 128bf62 commit 57a24ed

9 files changed

+327
-53
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ New crate containing public type definitions for the notify and debouncer crates
3434
- CHANGE: add `RecommendedCache`, which automatically enables the file ID cache on Windows and MacOS
3535
and disables it on Linux, where it is not needed
3636

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

3941
- 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
@@ -153,7 +153,7 @@ struct Queue {
153153

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

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

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

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

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

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

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

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

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

244-
self.queues = queues_remaining;
310+
self.queues.retain(|_, queue| !queue.events.is_empty());
245311

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

256316
events_expired
257317
}
@@ -426,18 +486,6 @@ impl<T: FileIdCache> DebounceDataInner<T> {
426486
source_queue.events.remove(remove_index);
427487
}
428488

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

458506
if let Some(target_queue) = self.queues.get_mut(&event.paths[0]) {
459-
if !target_queue.was_created() {
507+
if target_queue.was_removed() {
508+
let event = target_queue.events.pop_front().unwrap();
509+
source_queue.events.push_front(event);
510+
}
511+
512+
if !target_queue.was_created() && !source_queue.was_removed() {
460513
let mut remove_event = DebouncedEvent {
461514
event: Event {
462515
kind: EventKind::Remove(RemoveKind::Any),
@@ -474,6 +527,8 @@ impl<T: FileIdCache> DebounceDataInner<T> {
474527
} else {
475528
self.queues.insert(event.paths[0].clone(), source_queue);
476529
}
530+
531+
self.find_blocked_events(&event.paths[0]);
477532
}
478533

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

524598
/// Debouncer guard, stops the debouncer on drop.
@@ -756,6 +830,11 @@ mod tests {
756830
"add_remove_parent_event_after_remove_child_event",
757831
"add_errors",
758832
"emit_continuous_modify_content_events",
833+
"emit_create_event_after_safe_save_and_backup_override",
834+
"emit_create_event_after_safe_save_and_backup_rotation_twice",
835+
"emit_create_event_after_safe_save_and_backup_rotation",
836+
"emit_create_event_after_safe_save_and_double_move",
837+
"emit_create_event_after_safe_save_and_double_move_and_recreate",
759838
"emit_events_in_chronological_order",
760839
"emit_events_with_a_prepended_rename_event",
761840
"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)