A Simple Order Process with Azure WebJobs
The order system for Velocity uses Azure WebJobs behind the scenes. In this post, I'll explain how I use WebJobs and some things I've learned along the way.
Introduction
Azure WebJobs is really easy to work with. All you need to is build a console app, put in the functions you want available, and attach it to your website for automatic deployment. Scott gives a great intro in his Introducing Windows Azure Webjobs post, so I will try not to duplicate that here. I've found the Microsoft Azure Blog is the best resource for the latest information and as of the publishing this I'm looking the 0.4.0-beta preview.
One thing I will reiterate is how great the WebJobs control panel is. Through the panel you can see the status of running jobs. You can see a history of the various functions including inputs an outputs. You can choose to replay a particular function or enter the inputs manually and trigger a run. It also understands causality between functions so that it knows when a particular function was triggered by another.
Building an Order System
Azure WebJobs is setup to make it very easy to work with Queues and Blobs in Azure. I thought it would make a lot of sense to separate the actual order process from the website as much as possible in part to be as defensive against failures as I could. I was using Stripe for payments and Mailgun for transactional email. What happens when one of these services is down? Using a queue to manage the orders fits perfectly. A transient failure can occur and the message replayed.
When I first started to develop the order system I had one function that was basically "Order a License". The function would complete the charge with Stripe, create the license, and email a link to the customer. I quickly realized this is less than ideal. If, for example, the email were to fail I would have to come up with a different way to send the email. Replaying the function would either fail or create a duplicate charge and would not be helpful.
The obvious improvement was to separate each step into its own function. This resulted in three distinct functions:
1. OrderLicense
2. GenerateLicense
3. SendLicense
Now if the mail service were temporarily down I could simply replay the SendLicense function.
Order License
This completes the charge with Stripe then passes a message to the next queue to generate the license. I kept this function particularly simple because once someone has decided to make a purchase then completing the charge is extremely important. Everything else can be corrected or done manually, but I can't expect customers to make the purchase a second time.
Generate License
This function takes the information in the order and creates the license file. The license file is stored as a blob. This blob is registered as an output. Now I did overload this function a bit and also use it to store a record of the order and the license. I may need to refine this later, but so far this has been just fine. Once complete this function outputs a message to trigger the next function.
Send License
This function only sends the final email to the customer thanking them for their purchase and including a link to their license.
WebJob Magic
Authoring these functions is really easy thanks to the WebJob SDK. Each one is based on a QueueTrigger to start the function. They also have other inputs and outputs all of which are specified using special attributes on the parameters of the function. The GenerateLicense function for example writes a license entry to a table and generates the license file as a blob. These are simply handled as arguments to the function:
[QueueTrigger("generate")] Message message,
[Table("Licenses")] CloudTable licenses,
[Blob("licenses/{TransactionId}.license")] out string license
This kind of stuff makes working with Azure really easy. Note the use of {TransactionId}
in the Blob attribute. This automatically pulls the transaction id from the trigger message and uses it as the name of the blob. I simply set the string before exiting the function and the blob is created.
The send license function takes similar advantage of the WebJob attributes:
[Table("Orders", "{Email}", "{TransactionId}")] OrderEntity orderEntity
Again this uses special notations {Email}
and {TransactionId}
to automatically use values from the trigger message as the PartitionKey and RowKey for the table. The SDK then supplies the function with the matching row in the argument.
Ultimately, the Azure WebJobs SDK does a good job of letting me describe the function with its inputs and outputs in terms of Azure components. This creates expressive code and reduces boilerplate.
Beta Blues and Some Tips
There are some caveats to using WebJobs right now. It's a beta preview. I haven't found any particular issues with bugs, but the behavior can and will change from release to release. When releasing 0.4 for example, the Azure service was updated to use new connection string names. Use at your own risk and be attentive to changes.
As of this writing beware of using slots. I recently deployed a staging slot and a production slot. I eventually switched to move staging to production but this left the old version of my continously running job side by side with the new version. This can lead to bad things.
Failures in functions didn't always work as I expected. By design, a blob will be created as an output even on failure. Read the blog posts from the team carefully and test! Version 0.4 seems to handle some scenarios better and introduces automatic handling of poision messages (those that repeatedly fail).
When upgrading to 0.4 my orders started taking a lot longer to process. By design, the new version of the SDK will back off checking the queue when its empty. This can be controlled in configuration with the MaxPollingInterval setting.
Version 0.4 of the SDK includes a way to automatically deploy the WebJob project along with the website. Prior to this I used the WebJobsVs extension. Update regarding my publishing problems - DO NOT CHECK "Exclude files from the App_Data folder". At some point I did this and only on the production settings (smacks head), but App_Data is where jobs are published! Unfortunately for me the website included an unneeded reference to the webjob project so I would see the DLL copied into Bin during Publish and it didn't click right away that this wasn't the right place.
I used the Windows Azure Configuration Manager for all settings and connection strings. This allows me to keep my development settings in the config files, but use the Azure settings upon deployment.
Google around for some good information on debugging, but at least know that you can just run the console app locally in the debugger as long as the config is pointed to the right storage account. Don't forget to stop the jobs on any development slot pointed to the same account.
Wrap up
Be cautious about the beta preview status, but the Azure WebJobs SDK provides a powerful way to express the functions "jobs" that your website needs to perform in the background. I can imagine that it will become even more useful as it approaches release and seems like a no-brainer option if you are already using Azure.