Website Background Services Are Hot
This is a two-parter. The first part is to say, hey, look at this sweet hack I've discovered in the Oxite source*! The second part is to ask, hey, is this a good idea?
* the refactored Oxite source, that is
Background services
First, let's give a little detail here--background services are long-running tasks that Oxite needs to run periodically. These are things like sending emails and sending trackbacks--necessary, certainly. But, they shouldn't be running while some chump stares at his Netscape window waiting for the site to finish sending 1000 spam trackbacks. He should be able to post to his blog, receive an immediate response indicating the post is now available, and the trackback spamming can commence later. Background services are the things you can put off, the things that don't have to finish before sending a response to your website patrons.
These background services are called by many names--I've heard cron jobs, timer jobs, background jobs, jobs, "the heartbeat," services, and tasks. In Oxite they're called background services.
Look at this sweet hack!
The full source is below, but I'll attempt a walkthrough of the solution here. First, to explain the problem: we must achieve the impossible--we must somehow emulate a continuously-running Windows service inside an IIS worker process. This means we must periodically trigger jobs to run, but we can't monopolize valuable worker threads. And we certainly can't delay responses to send 30000 spam trackbacks. We've got to run, but we can't run anywhere in the ASP.NET page/request lifecycle! It's a conundrum.
What the Oxite team has done to achieve the impossible is, plainly, to cheat--they use a System.Threading.Timer.
How they manage the impossible is a lot like juggling--magic juggling. Enter stage left: Oxite, the juggler. Oxite takes a background task and throws it in the air. He takes hold of the next background task (let's start calling these things bowling pins) and throws it into the air, and moves on down the line. Before anyone knows what's happened, Oxite has gathered up all the bowling pins, thrown them all into the air, and made his getaway. Unlike most jugglers, Oxite makes no attempt to catch bowling pins once thrown! And this is why it's magic.
Let's try to break this back down into code. When Start() first executes [line 28], the Timer object sets a callback without halting progress [line 43]. This is the juggler throwing a pin in the air.
The callback method is eventually invoked. A thread is spun up* and runs the designated timerCallback() function [line 56]--and, let's make this clear--timerCallback() doesn't block the original Oxite web request; it lives in a new thread. And this new thread does its first dose of work, as shown on line 68 (SPOILER ALERT: it calls Run()). We're not interested in what Run() does exactly--for today it must remain a spooky mystery, go look it up yourself.
* precisely how the thread is spun up is in fact, real magic, or might as well be to my superstitious caveman brain
Ok. Here's where the "magic" part of magic juggling comes in. Because any dunce can throw bowling pins, and any dunce can catch them, and any dunce, with practice, can juggle. The magic here is inside the timerCallback() method, where the Timer once again sets a callback. Each time a background service awakens, it does its work and, before going back to sleep, sets up the next callback with another call to timer.Change() [line 75]. That is to say, each time the bowling pin makes as if to land, it spins back upward into the air!
So there you have it. Oxite takes a bunch of bowling pins, throws them all into the air, and leaves. As the pins drop down to the ground, the "mystical Timer callback juggling force" propels them back into the air.
And we're running background threads in the web process. Sweet.
Now the question is: is this a good idea?
Now you understand how background tasks work in Oxite--or can now juggle. I get confused sometimes. In any case, congratulations!
Assuming I'm not misrepresenting anything, this is how background tasks work in Oxite. So, now for the question. Is this a reasonably acceptable way to set up background tasks for a site? I've discussed it some on twitter, but is there anything particularly nasty I've missed? Will it kill the process? Will it hang all 25 threads? Or some large portion of them?
I'm curious to hear if anyone has taken this approach, and what their experiences were.
Full source
From http://oxite.codeplex.com/SourceControl/changeset/view/45053#438025:
1 // --------------------------------
2 // Copyright (c) Microsoft Corporation. All rights reserved.
3 // This source code is made available under the terms of the Microsoft Public License (Ms-PL)
4 // http://www.codeplex.com/oxite/license
5 // ---------------------------------
6 using System;
7 using System.Threading;
8 using Microsoft.Practices.Unity;
9 using Oxite.Services;
10
11 namespace Oxite.Infrastructure
12 {
13 public class BackgroundServiceExecutor
14 {
15 private readonly Timer timer;
16 private readonly IUnityContainer container;
17 private readonly Guid pluginID;
18 private readonly Type type;
19
20 public BackgroundServiceExecutor(IUnityContainer container, Guid pluginID, Type type)
21 {
22 this.timer = new Timer(timerCallback);
23 this.container = container;
24 this.pluginID = pluginID;
25 this.type = type;
26 }
27
28 public void Start()
29 {
30 IBackgroundService backgroundService = (IBackgroundService)container.Resolve(type);
31 IPlugin plugin = getPlugin();
32 TimeSpan interval = getInterval(plugin);
33
34 if (interval.TotalSeconds > 10)
35 {
36 #if DEBUG
37 if (plugin.Enabled)
38 {
39 backgroundService.Run(plugin.Settings);
40 }
41 #endif
42
43 timer.Change(interval, new TimeSpan(0, 0, 0, 0, -1));
44 }
45 }
46
47 public void Stop()
48 {
49 lock (timer)
50 {
51 timer.Change(Timeout.Infinite, Timeout.Infinite);
52 timer.Dispose();
53 }
54 }
55
56 private void timerCallback(object state)
57 {
58 lock (timer)
59 {
60 IBackgroundService backgroundService = (IBackgroundService)container.Resolve(type);
61 IPlugin plugin = getPlugin();
62 TimeSpan interval = getInterval(plugin);
63
64 if (plugin.Enabled)
65 {
66 try
67 {
68 backgroundService.Run(plugin.Settings);
69 }
70 catch
71 {
72 }
73 }
74
75 timer.Change(interval, new TimeSpan(0, 0, 0, 0, -1));
76 }
77
78 //TODO: (erikpo) Once background services have a cancel state and timeout interval, check their state and cancel if appropriate
79 }
80
81 private IPlugin getPlugin()
82 {
83 IPluginService pluginService = container.Resolve<IPluginService>();
84 IPlugin plugin = pluginService.GetPlugin(pluginID);
85
86 return plugin;
87 }
88
89 private TimeSpan getInterval(IPlugin plugin)
90 {
91 return TimeSpan.FromTicks(long.Parse(plugin.Settings["Interval"]));
92 }
93 }
94 }