Backing up del.icio.us and launchd
Since computers are good at doing things periodically, I figured I'd hand this job over to
cron, the standard Unix scheduling tool. First, I wrapped the whole thing as a Ruby script. I wanted to grab the password from Keychain, as I described earlier, instead of putting the password in the script itself, which is easy in Ruby. I also wanted to add some logging, which is also easy in Ruby. Next, I tell
cron to run this script every morning at 4am.
cron is old school. It doesn't behave well when the machine isn't on 24x7. Apparently the original designers of Unix couldn't imagine a Unix machine not running 24x7. But I run Mac OS X on my laptop. And I put my laptop to sleep when I'm not using it. So guess what I have? A Unix machine that isn't on 24x7. If I schedule a job to be run at, say 4am, but my machine is asleep at 4am,
cron blissfully ignores this job. Apple, too, ignored this problem for Mac OS X versions 10.0 through 10.3. However, in 10.4, they introduced
launchd is an über-daemon of sorts, taking over the duties of not only
cron, but also
mach_init, and it replaces
init as PID 1, the very first process the Unix kernel executes. And since this was replacing
cron, why not fix any of the cruftiness that came with it? According to launchd.plist(5), you can use StartCalendarInterval to emulate
cron (added emphases is mine):
This optional key causes the job to be started every calendar interval as specified. Missing arguments are considered to be wildcard. The semantics are much like crontab(5). Unlike cron which skips job invocations when the computer is asleep, launchd will start the job the next time the computer wakes up. If multiple intervals transpire before the computer is woken, those events will be coalesced into one event upon wake from sleep.
Great! I'm thinking I'll just use
launchd instead of
cron, and everything will be rosy. I read a tutorial on how to setup a
launchd plist by hand. Then I quickly started using Lingon as a GUI that makes this easier. I ran a couple tests to make sure it's all working. Finally, I put my computer to sleep to call it a day, knowing that when I wake up, it'll run this script. Not so fast.... I blissfully wake my computer the next morning over a cup of tea, only to check my log files and find out the script hasn't run. What the..... I thought
launchd was supposed to fix this crap? It said so in the
man page! So I spent an hour or two trying various things. I enabled
launchd logging. Did I mess up the plist? Put in the wrong time? Forget to enable it? Nope. It turns out
launchd is just plain broken.
After much searching of Apple mailing lists and Googling the rest of the Internet figuring I haven't been the first person to run across this issue, I finally found a discussion thread with a quote allegedly from an Apple engineer:
[T]here is another bug (4058640) that he should be aware of when using either of the interval based launch mechanisms. launchd let's the kernel maintain all the timers, and as a consequence, any timer bug in the kernel, therefore affects any program that uses said mechanism (launchd included). In this case, the kernel will delay a timer from firing the amount of time that the computer is asleep. Thus, if you sleep your computer for an hour during the day, and then leave it running overnight, a job scheduled to fire at 2am will actually run at 3am. Also, please note, the drift only happens for the amount of time slept between when the timer was setup and when it goes off next. If the computer never goes to sleep thereafter, all subsequent alarms will occur at the correct time
Gah! So if I put my computer to sleep for 8 hours, instead of running at 4am, it'll run at noon. But this was an old post from May of 2005, almost a year ago. Surely Apple has fixed this embarrassing and obvious issue, right? Again... Nope! I'm running 10.4.5, and it's not fixed. This is confirmed by another person on Apple's discussion boards.
What to do... Well,
launchd isn't the only solution to this issue. The Linux guys have been using something called
anacron for years now. I could just pull down the source code, compile it, and run it via
launchd. If only it was that easy. First off, the idiotic
anacron home page doesn't even have a link to its source code. When you do finally get the source, it's very Linux specific, and doesn't compile on OS X. Fine, I'll just grab the binary from Fink. Now I just need to run it via
launchd, and run it every hour. I'll have it use my own personal config file, which I put in
~/Library/etc/anacrontab. It turns out this doesn't work because
anacron needs to store its state information in
/sw/var/spool/anacron. Since this directory is writable only as root, it doesn't have proper permission to use it. Surely this was configurable, right? If you said "Nope!" you're getting good at this game. This path is hardcoded at compile time. If I could actually compile this damn thing, this would be an easy fix, but, as I mentioned earlier, it doesn't compile out-of-the-box on OS X.
I could have grabbed the Fink patches for
anacron and compiled it myself, but I was fairly annoyed at this point. Besides, if I was going to run
anacron once an hour, I may as well just run my backup script once an hour. Annoyed as I was, it wasn't at del.icio.us, so I figured I ought to be nice to their servers. The del.icio.us API recommends calling http://del.icio.us/api/posts/update prior to http://del.icio.us/api/posts/all? to only pull all the posts when needed. I needed to update my script to handle this. Instead of calling
curl from Ruby, I modified it to use Net::HTTP so I could more easily read and parse the output from posts/update.
I've posted the final Ruby script,
backup_delicious, and my
launchd plist for those interested. It is worth noting that running something on the top of the hour like this does suffer from the kernel timer bug. Thus, if I put my machine to sleep at 11:40pm, the script gets run 20 minutes after I wake my machine. This will run often enough where it won't matter, though. I haven't yet upgraded to 10.4.6, and perhaps this is fixed there. Apple doesn't seem to be particularly concerned about this bug, so I wouldn't be surprised if it still exists. I'll leave you with this one bit of advice: don't count on
launchd to solve all your
cron issues just yet.