-
Notifications
You must be signed in to change notification settings - Fork 3
🔨 Remove potential cross-threads contention by eliminating Mutex on Batch Processor #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
316a73a
b52fac3
1f4fc25
674f272
15e247c
bb84a81
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,6 @@ | ||||||||||||||||||||||||||||||||||||||
| use std::io::ErrorKind as IOErrorKind; | ||||||||||||||||||||||||||||||||||||||
| use std::path::{Path, PathBuf}; | ||||||||||||||||||||||||||||||||||||||
| use std::sync::{Arc, Mutex}; | ||||||||||||||||||||||||||||||||||||||
| use std::time::Duration; | ||||||||||||||||||||||||||||||||||||||
| use std::time::{Duration, Instant}; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| use crate::events::EventType; | ||||||||||||||||||||||||||||||||||||||
| use crate::events::access::from_access_kind; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -16,7 +15,7 @@ use notify::{ | |||||||||||||||||||||||||||||||||||||
| use pyo3::exceptions::{PyException, PyFileNotFoundError, PyOSError, PyPermissionError}; | ||||||||||||||||||||||||||||||||||||||
| use pyo3::prelude::*; | ||||||||||||||||||||||||||||||||||||||
| use tokio::{ | ||||||||||||||||||||||||||||||||||||||
| sync::{broadcast, oneshot}, | ||||||||||||||||||||||||||||||||||||||
| sync::{broadcast, mpsc, oneshot}, | ||||||||||||||||||||||||||||||||||||||
| time, | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
| // use crate::file_cache::FileCache; | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -25,13 +24,18 @@ use crate::processor::{BatchProcessor, EventProcessor, RawEvent}; | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| pyo3::create_exception!(_inotify_toolkit_lib, WatcherError, PyException); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| type TimestampedEvent = (Instant, Result<Event, notify::Error>); | ||||||||||||||||||||||||||||||||||||||
| type EventReceiver = mpsc::Receiver<TimestampedEvent>; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| #[derive(Debug)] | ||||||||||||||||||||||||||||||||||||||
| pub(crate) struct Watcher { | ||||||||||||||||||||||||||||||||||||||
| debug: bool, | ||||||||||||||||||||||||||||||||||||||
| event_buffer_size: usize, | ||||||||||||||||||||||||||||||||||||||
| buffering_duration: Duration, | ||||||||||||||||||||||||||||||||||||||
| inner: RecommendedWatcher, | ||||||||||||||||||||||||||||||||||||||
| // file_cache: FileCache, | ||||||||||||||||||||||||||||||||||||||
| processor: Arc<Mutex<BatchProcessor>>, // TODO: use the EventProcessor trait instead | ||||||||||||||||||||||||||||||||||||||
| event_rx: Option<EventReceiver>, | ||||||||||||||||||||||||||||||||||||||
| rx_return: Option<oneshot::Receiver<EventReceiver>>, | ||||||||||||||||||||||||||||||||||||||
| tx: broadcast::Sender<Vec<EventType>>, | ||||||||||||||||||||||||||||||||||||||
| stop_tx: Option<oneshot::Sender<()>>, | ||||||||||||||||||||||||||||||||||||||
| drain_handle: Option<tokio::task::JoinHandle<()>>, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -44,35 +48,20 @@ impl Watcher { | |||||||||||||||||||||||||||||||||||||
| debug: bool, | ||||||||||||||||||||||||||||||||||||||
| follow_symlinks: bool, | ||||||||||||||||||||||||||||||||||||||
| ) -> Result<Self, notify::Error> { | ||||||||||||||||||||||||||||||||||||||
| // TODO: hide usage of file cache from Watcher | ||||||||||||||||||||||||||||||||||||||
| // let file_cache = FileCache::new(); | ||||||||||||||||||||||||||||||||||||||
| // let file_cache_c = file_cache.clone(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let buffering_duration = Duration::from_millis(buffering_time_ms); | ||||||||||||||||||||||||||||||||||||||
| let processor = Arc::new(Mutex::new(BatchProcessor::new(buffering_duration))); | ||||||||||||||||||||||||||||||||||||||
| let processor_c = processor.clone(); | ||||||||||||||||||||||||||||||||||||||
| let (event_tx, event_rx) = mpsc::channel(event_buffer_size); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let (tx, _rx) = broadcast::channel::<Vec<EventType>>(event_buffer_size); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let config = notify::Config::default().with_follow_symlinks(follow_symlinks); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let inner = RecommendedWatcher::new( | ||||||||||||||||||||||||||||||||||||||
| move |e: Result<Event, notify::Error>| { | ||||||||||||||||||||||||||||||||||||||
| let mut event_processor = match processor_c.lock() { | ||||||||||||||||||||||||||||||||||||||
| Ok(guard) => guard, | ||||||||||||||||||||||||||||||||||||||
| Err(e) => { | ||||||||||||||||||||||||||||||||||||||
| eprintln!("notifykit: event processor lock poisoned, dropping event: {e}"); | ||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| if debug { | ||||||||||||||||||||||||||||||||||||||
| println!("raw event: {:?}", e); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| match e { | ||||||||||||||||||||||||||||||||||||||
| Ok(e) => event_processor.add_event(e), | ||||||||||||||||||||||||||||||||||||||
| Err(e) => event_processor.add_error(e), | ||||||||||||||||||||||||||||||||||||||
| if let Err(e) = event_tx.try_send((Instant::now(), e)) { | ||||||||||||||||||||||||||||||||||||||
| eprintln!("event channel full or closed, dropping event: {e}"); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||
| config, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -81,8 +70,10 @@ impl Watcher { | |||||||||||||||||||||||||||||||||||||
| Ok(Self { | ||||||||||||||||||||||||||||||||||||||
| debug, | ||||||||||||||||||||||||||||||||||||||
| event_buffer_size, | ||||||||||||||||||||||||||||||||||||||
| buffering_duration, | ||||||||||||||||||||||||||||||||||||||
| inner, | ||||||||||||||||||||||||||||||||||||||
| processor, | ||||||||||||||||||||||||||||||||||||||
| event_rx: Some(event_rx), | ||||||||||||||||||||||||||||||||||||||
| rx_return: None, | ||||||||||||||||||||||||||||||||||||||
| tx, | ||||||||||||||||||||||||||||||||||||||
| stop_tx: None, | ||||||||||||||||||||||||||||||||||||||
| drain_handle: None, | ||||||||||||||||||||||||||||||||||||||
|
|
@@ -149,47 +140,64 @@ impl Watcher { | |||||||||||||||||||||||||||||||||||||
| let _ = tx.send(()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| self.recover_event_rx(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let (new_tx, _rx) = broadcast::channel::<Vec<EventType>>(self.event_buffer_size); | ||||||||||||||||||||||||||||||||||||||
| self.tx = new_tx; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| fn recover_event_rx(&mut self) { | ||||||||||||||||||||||||||||||||||||||
| if let Some(handle) = self.drain_handle.take() { | ||||||||||||||||||||||||||||||||||||||
| handle.abort(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| let (new_tx, _rx) = broadcast::channel::<Vec<EventType>>(self.event_buffer_size); | ||||||||||||||||||||||||||||||||||||||
| self.tx = new_tx; | ||||||||||||||||||||||||||||||||||||||
| if let Some(mut rx_return) = self.rx_return.take() { | ||||||||||||||||||||||||||||||||||||||
| if let Ok(rx) = rx_return.try_recv() { | ||||||||||||||||||||||||||||||||||||||
| self.event_rx = Some(rx); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| if let Some(mut rx_return) = self.rx_return.take() { | |
| if let Ok(rx) = rx_return.try_recv() { | |
| self.event_rx = Some(rx); | |
| if let Some(rx_return) = &mut self.rx_return { | |
| match rx_return.try_recv() { | |
| Ok(rx) => { | |
| // Successfully recovered the event receiver; we no longer need rx_return. | |
| self.event_rx = Some(rx); | |
| self.rx_return = None; | |
| } | |
| Err(tokio::sync::oneshot::error::TryRecvError::Empty) => { | |
| // The drain task has not yet returned the receiver; keep rx_return so | |
| // that a future call to recover_event_rx can try again. | |
| } | |
| Err(tokio::sync::oneshot::error::TryRecvError::Closed) => { | |
| // The oneshot will never yield a receiver; drop it to avoid retrying. | |
| self.rx_return = None; | |
| } |
Copilot
AI
Mar 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
start_drain() silently returns when self.event_rx is None. With the current “move receiver into task and return it later” design, this can happen after a stop/restart attempt or if recover_event_rx() couldn’t immediately reclaim the receiver. Instead of returning, consider blocking until the receiver is available (or recreating the channel/sender pair) so callers don’t end up with a watcher that never emits events and only logs dropped events.
Copilot
AI
Mar 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
BatchProcessor is now created inside the drain task. When stop_and_recover stops a running drain, any events already moved from event_rx into processor but not yet emitted will be dropped when the task exits. Previously buffered events survived drain restarts because the processor lived outside the task. If drain restarts are expected (e.g., multiple events() calls), consider persisting the processor across drain lifecycles or flushing pending buffered events back into the receiver/channel before exit.
Copilot
AI
Mar 9, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The drain task only consumes from event_rx on each ticker.tick() via try_recv(). If tick_duration is large or events arrive in bursts, the bounded channel can fill up and try_send will start dropping events even though the drain task is idle between ticks. Consider also awaiting event_rx.recv() (and buffering timestamps) so the receiver keeps up continuously while still emitting batches on tick boundaries.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The log message says "dropping event" but
{e}here is aTrySendErrorwhoseDisplaytypically only reports the reason (full/closed), not the event being dropped. Consider logging the dropped payload (or using{:?}and matchingFull/Closed) so debug output is actionable.