snooze: runit/s6 as a cron replacement
One cron alternative I don't see mentioned often is snooze. Despite doing away with crontabs (which means it's not entirely compatible with cron), in my opinion it makes up for it with an interesting design that makes it more versatile and more scripting-friendly.
While the manual is very clear (and a recommended reading if you want to understand it on a deeper level), I hope this becomes an gentler introduction.
The Principle
Crons act as a single centralized database for all scheduled tasks on the system. Therefore, each system must have one (and only one) instance of cron running, and it does so as a daemon that stays operational at all times. To use Cron's scheduling powers, you need to add an entry for your script to the crontab. Systemd timers work similarly.
In contrast, snooze is designed to work as a "sleep on steroids". Each scheduled task is a script that invokes the snooze command to sleep until it's time to perform the relevant task. Therefore, the system will have a swarm of snoozes, and its power is available from any programming language where spawning and waiting for additional processes is supported.
Usage
By default, snooze will wait until the next midnight. To override this behavior, it has various options, each corresponding to a field in the crontab. For example, to run the /usr/local/bin/cih.sh command at April 26, 6:30PM:
snooze -d26 -m4 -H18 -M30 -- /usr/local/bin/cih.sh
Which is equivalent to cron's
30 18 26 4 * /usr/local/bin/cih.sh
Snooze also accepts cron-style ranges (i.e. 14,16 for both 2PM and 6 PM, 18-20 for 6, 7 or 8 PM, or any combination of commas and hyphens) as well as cron-style repeaters (i.e. /5 for every 5 hours/days, etc.) for every option.
Notice that snooze works with easy-to-memorize options, rather than a fixed-order crontab (which yielded me quite a few trips to man 5 crontab).
Either way, once the time is right, snooze will either exit (if no additional arguments are passed), or, if additional arguments are passed, directly execute them as programs.
From my description, you might already see how snooze can fit in places Cron's "centralized database on a centralized process" approach makes it cumbersome. Want to send yourself a one-off notification at 4 PM? You can call snooze -H16 -- notify-send 'make a snack' on a terminal. Snooze will inherit all environment variables from your user session, so (on a desktop system) things like D-Bus, IPC to your window manager, etc. will work fine — no need for complicated math or pattern matching with the output of the date command, or to arrange environment variables on the crontab. It's possible to even set up a while true loop make snooze start again after sending the notification, making it a daily reminder.
By exploiting runit/s6's automatic restarts, dinit's restart=yes setting or OpenRC's supervise-daemon, you can turn cronjobs into regular system services. Since it uses the exec system call, the process that spawned snooze (your service manager) will know the exit status of the cronjob and you'll be able to control it with signals.
For more (admittedly minor in "real world" usage) extra features, snooze also works with seconds (-S) and days/weeks of the year (-D/-W).
Slack and time files: simulating anacron
By default, snooze will try to find, starting from the current date and time, the next second matching the conditions specified in its options, and sleep until it comes. However, there are tasks that must be run once daily, weekly or monthly, no matter the actual hour, and restricting then to a specific hour of a specific day isn't efficient. Cron implementations solve this with an additional program, anacron. Snooze's solution involves two settings:
- the time file, specified with the -t option. If told to use one, snooze will look for times matching the given conditions starting from when the file was last modified (or, in Linux/Unix lingo, the mtime), rather than when this specific instance of snooze was started.
- the slack, specified with the -s option (which can be either be in seconds, or a whole number of days, hours or minutes if the units d, h or m are mentioned). If there's a match that's in the past, but still within slack seconds of the present, snooze will consider it a successful match anyway and run the command/exit.
For example, a way to ensure a task is run daily is using the default settings (run every midnight), except for a slack of exactly one day, and a timefile that is modified (with the touch command) every time the task is run. This way, if snooze is started anywhere from 00:00:00 to 23:59:59 of the day after the task was last run, the task will be immediately started, and if not, it'll wait until midnight. If the OS is shutdown before midnight, well, there's literally the entirety of the next day for that.
This is basically the principle behind an anacron implemented with snooze. In fact, the sv subdirectory of snooze's GitHub repository is pretty much an anacron implemented with snooze, using timefiles in /var/cache/snooze and the run-parts command that is spawned by most anacron implementations. It's ready to use for runit, and by using an additional type file with the word "longrun" as its contents, it can be used with s6-rc.
Wrapping up
snooze is basically what cron would be if it was designed by a DJB fan. Snooze doesn't want to be a centralized database of all scheduled tasks in your system; it's intended to use the power of multiple processes and exec to work its way into anywhere that a scheduled task might be useful. For that, I think it's ultimately a better design than cron, despite the concerns that multiple processes and timefiles might be less efficient. The gains in versatility are worth it, IMO.
Feel to make any corrections (especially me misremebering how cron works) or relevant comments.