Recently, in a fit of narcissism, I decided that my blog needed to be Twitter-aware; that is, I wanted all my posts to display automatically-generated lists of all Twitter tweets that reference them (via the backtweets.com API). Loading these backtweets is a bit resource-intensive, so I figured it’d be best to offload the work to a scheduled background task (e.g., a cron job). Since I figure I’ll be implementing more such tasks in the future, I took the opportunity to build out a simple cron task architecture that can handle any number of such jobs with relative ease. Here’s how it works.
Now, any cron task we might envision will most likely need access to a fully-bootstrapped instance of the application; that way we’ll be able to use all of the framework’s usual development conveniences. However, loading the full MVC architecture is a bit of overkill, since we’re going to be performing the same basic sequence of tasks every time the script is hit (see Matthew Weier-O’Phinney’s recent discussion of service APIs for some other relevant concerns). So, we’re going to need a new application bootstrap and entry point, one that eschews the MVC routing and dispatch process in favor of something simpler. Essentially, all we’ll need is to be able to run an arbitrary collection of cron “task plugins,” the list of which can be configured in plain text via any of the various Zend_Config formats (e.g., the default application.ini file).
Here, then, is the plan, broadly stated:
- Cron will be accessible via its own distinct entry point (say, services/cron.php).
- The cron entry point will
Zend_Applicationinstance using a custom bootstrap class.
- Instead of running through the usual MVC routing/dispatching cycle, the bootstrap class will configure and run a cron service class, returning its output to the user.
- The cron service class will run a set of arbitrary cron task plugins assigned to it in the bootstrap from the application-level config file.
The task plugin interface
Let’s look at the simplest piece first: the task plugin interface. Each task plugin should have a
run() method containing the code that performs its task, along with a constructor that accepts any arguments the task may require (e.g., my backtweet loader task requires a backtweets.com API key). The interface is very simple:
1 2 3 4 5 6 7 8 9 10 11 12 13
For example, if for some reason you wanted to update the timestamp on a particular file each cron run, you could do so like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
Contrived example, I know, but it should demonstrate the usage pretty well.
The cron service
Once we’ve got our task plugin classes built, we’ll need a service that knows how to instantiate and run them. Since we want to be able to list and configure our cron task plugins in an INI file, we’ll leverage
Zend_Loader_PluginLoader; that way the calling code will only need to know the short names of the task plugins (“TouchFile”, “LoadBackTweets”, etc.).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91
Most of that should be pretty straightforward if you’ve ever worked with
Zend_Loader_PluginLoader; essentially, we allow the calling code to register tasks via the
addAction() method, specifying their name and any arguments that should be passed to the constructor. Then, when the whole process is
run(), we let
Zend_Loader_PluginLoader find and load the class, instantiate it with the appropriate arguments, and run it.
Bootstrapping and running it all
So far so good. We have a service that’s capable of running an arbitrary collection of cron tasks; now we just need something that can run the service. This will just be a simple, lightweight extension of the main bootstrap class; instead of setting up the MVC router, dispatcher and so forth, it’ll just instantiate our cron service, configure it according to what’s in application.ini, and run the thing.
For the configuration step, we can easily leverage one of Zend_Application’s more powerful features: the resource plugin. Resource plugins allow you to define how application-level resources are to be set up and configured (for a good concise introduction to the concept, see this post from Rob Allen). In this system, we’ll need to be able to configure three basic things: (1) where our task plugins are located, and (2) which task plugins to run, and (3) what arguments they should take. The class turns out to be pretty simple:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
With that resource available, you can set up your cron tasks as you like in application.ini. Here are a couple of examples:
From there, we can set up a simple custom bootstrap class whose
run() method tells the cron service to do its business:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Finally, we need to set up a separate entry point that knows to use this modified bootstrap class. This can be anywhere in your public webroot; your cron daemon can then use curl to initiate the script (though you may want to configure your web server to only allow access from the machine with the cron daemon on it). Your entry point, wherever it’s located, will look very similar to your main index.php; the only difference is in how you instantiate the Zend_Application object:
1 2 3 4 5 6 7 8 9 10 11 12 13
And that’s pretty much it! Now that all that infrastructure is in place, it’s easy to add new cron tasks as desired …just implement a new
Blahg_Plugin_Cron_CronInterface class in your cron task plugin directory, add the appropriate configuration settings to application.ini, and the cron service will take care of the rest.
After reading all this you may be saying to yourself, “gee, that seems like a lot of files.” Or you may not. Either way, I thought it’d be worth listing off the names of the source files I’ve mentioned, and where they’re located in my directory structure; most of these will be picked up by a standard ZF resource autoloader, so if your application’s already configured with one of those you shouldn’t run into any weird issues.
- public/path/to/cron.php (doesn’t really matter where this is as long as it’s in the webroot)
Now, this system isn’t quite finished yet. Probably the biggest omission is a locking system to circumvent problems caused by cron run overlaps (see this recent post from Abhinav Singh for more information). Still, I think it’s a very good step in the right direction.
If I get a chance I may try to turn this into a full-fledged Zend Framework proposal. In the meantime, if you’d like to use this code now, feel free to copy and paste:
All source code in this article is by Adam Jensen, and is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.
Thanks for reading!