Advertisement
Open Source Projects by Phil Schwartz

Building a reliable Python INotify callback queue

The process for how I wrote a Python module to handle INotify events with a callback queue began with a practical requirement: report filesystem changes without making every caller understand Linux’s low-level notification API. I wanted a Python-friendly layer that delivered structured events and stayed dependable when a consumer was slow or faulty.

The Linux kernel’s inotify interface watches files and directories. It reports creations, modifications, closes, deletions, moves and attribute changes through a file descriptor. Its strengths are low overhead and prompt delivery, but the raw API uses binary records, numeric watch descriptors and several limits that application code must respect.

A module also needs to separate receiving events from running user code. That distinction led to the callback queue: one component reads and decodes kernel records, while another delivers them in order. Slow processing therefore cannot make the intake path depend on a callback returning immediately.

The design had to work in Australian environments as well as on a developer’s machine. Linux servers in Sydney, Melbourne and Brisbane often serve teams using Windows or macOS laptops, where path casing and rename behaviour can differ from a case-sensitive Linux filesystem. The public API needed to hide those differences without hiding real errors.

Why I built another filesystem watcher

Much of my open-source work follows the same pattern. DenyHosts turns SSH log activity into protective action, Kodos makes regular expressions easier to inspect, and Scratchy analyses Apache logs. Each tool starts with an operating-system signal and ends with a focused interface, so the inotify module belonged in that family rather than becoming a general-purpose file framework.

The first design choice was scope. Watches apply to individual directories and are not recursive unless each directory is registered. Rename events may arrive as separate move-away and move-in records joined by a cookie, and a watched file can be replaced without changing its path. Representing those cases explicitly prevented callbacks from receiving an ambiguous stream of names.

Turning kernel notifications into Python events

I used Python’s ctypes bindings to call libc rather than depend on a large native extension. The wrapper covered inotify initialisation, watch registration and removal, and set the descriptor to non-blocking mode. This kept installation light and made the Linux-specific boundary visible in the source.

Each read returns one or more variable-length event structures. The parser extracts the watch descriptor, event mask, rename cookie and name length, then removes the trailing null padding before decoding the filename. A dictionary maps watch descriptors back to their registered paths, allowing the module to expose useful absolute or caller-relative paths without repeatedly walking the filesystem.

Readiness came through Python’s selectors module, which can use epoll on Linux. The event loop waits for activity, reads only available bytes and processes a complete batch before waiting again. This avoids a busy loop while still allowing the worker thread to own callback execution and keeping kernel-facing code separate from application-facing objects.

Separating event intake from callback execution

The callback queue is bounded and ordered. The reader thread places decoded events into it, and one worker thread invokes registered handlers. Serial delivery removes a common class of race conditions when several handlers mutate shared state, while the bound prevents an unnoticed flood from consuming unlimited memory.

Backpressure needs an explicit policy. Blocking the reader can eventually exhaust the kernel queue, while dropping events silently can corrupt an audit trail. I made queue pressure observable and selected the least harmful behaviour for the caller, such as coalescing repeated changes to the same path or reporting that a resynchronisation is required.

Callback failures are isolated too. An exception is logged with context and does not terminate the worker, while other subscribers continue receiving later events. Shutdown uses a sentinel, waits for queued work to drain and prevents new callbacks from starting after the module has closed.

Testing and deploying the module

Tests used temporary directories to create, edit, rename and remove files, including names with spaces and long suffixes. Mocked system calls covered malformed records, descriptor failures and a full queue. Linux-only integration tests exercised the real kernel API and checked watch limits, including the possibility of an inotify queue-overflow notification.

Operational timestamps are stored in UTC. That matters across Australia: Sydney and Melbourne change their clocks for daylight saving, while Brisbane remains on UTC+10 throughout the year. Using UTC for logs and callbacks avoids duplicated or missing local times when machines in different cities exchange records.

File watching also deserves privacy-aware defaults. Event names can expose details about customer or health records, and covered Australian organisations may need to consider the Privacy Act 1988 and the Notifiable Data Breaches scheme. The module should watch only necessary paths, avoid recording names by default and make any retained metadata easy to secure and delete.

What the open-source release taught me

The release followed the approach used across my project pages: explain the purpose, show the licence, provide source and installation options, and publish download statistics. A compact module also suits the Australian market for small, Linux-focused engineering teams that want maintainable open-source tooling without a heavy dependency tree, even though the module remains deliberately Linux-only.

The most valuable lesson was to make concurrency a public design decision. Callbacks are ordinary Python functions, but their timing, ordering and failure behaviour are part of the contract. Defining those boundaries first produced a module that is easier to test, embed in services such as file monitors, and trust in production.