inotify-rsync: push files to other routers the moment they change
Some files on my hubs are produced in one place: one hub renders a few static files and downloads a few big ones, and the other hubs serve copies. The copies should follow quickly. A cron job means either lag or a lot of pointless runs, so I wanted rsync to fire when something actually changes.
inotify-rsync does exactly that and nothing more: watch some directories, run rsync to a destination when they change.
Configuration
One UCI section per watch:
# /etc/config/inotify-rsync
config watch 'site'
option enabled '1'
list src '/data/site'
option dest 'root@peer.example.org:/data/'
option rsync_args "-Ppra --delete --delete-delay -e 'dbclient -i /root/.ssh/id_ed25519 -y'"
list filter 'exclude *.tmp'
option textfile_dir '/run/prometheus/textfile'
Each watch is its own procd instance, respawned if it dies.
/etc/init.d/inotify-rsync rsync [watch] forces a sync by hand, which is
handy right after deployment. On OpenWrt the ssh client is usually Dropbear’s
dbclient, so rsync_args is split respecting quotes — the -e '…' above
stays one argument.
From inotifyd to ucode
The first version was a shell script around BusyBox’s inotifyd applet. It
worked, but inotifyd runs as a child process, and when procd killed the
script on restart the child was left behind — orphaned watchers piling up.
So in August 2026 it was rewritten in ucode, and I wrote a small C module for it, ucode-mod-inotify, that exposes the Linux inotify API to ucode directly. The script now:
- opens one inotify descriptor and adds a watch for every directory under the sources (collected at start);
- waits on it in the
uloopevent loop — no subprocess at all; - debounces: a burst of writes restarts a 100 ms timer, so a whole batch of files becomes one rsync run instead of dozens.
Metrics
With textfile_dir set, every watch writes a small .prom file for the
node exporter’s textfile collector:
inotify_rsync_events_total— filesystem events seen;inotify_rsync_runs_totalandinotify_rsync_errors_total;inotify_rsync_last_run_seconds— the timestamp of the last run.
The counters are read back on start, so they survive restarts, and the file is replaced atomically. Two alerts are enough to watch it: errors increasing, and a last run that is suspiciously old.