现在的位置: 首页 > 综合 > 正文

Simulate a Windows Service using ASP.NET to run scheduled jobs

2011年12月25日 ⁄ 综合 ⁄ 共 5850字 ⁄ 字号 评论关闭

Introduction

How to run
scheduled jobs from ASP.NET without requiring a Windows Service to be installed
on the server? Very often we need to run some maintenance tasks or scheduled
tasks like sending reminder emails to users from our websites. This can only be
achieved using a Windows service. ASP.NET being stateless provides no support
to run some code continuously or to run code at a scheduled time. As a result,
we have to make our own Windows Services in order to run scheduled jobs or cron
jobs. But in a shared hosted environment, we do not always have the luxury to
deploy our own Windows service to our hosting provider’s web server. We either
have to buy a dedicated server which is very costly, or sacrifice such features
in our web solution. However, running a scheduled task is a very handy feature
especially for sending reminder emails to users, maintenance reports to
administrators, or run cleanup operations etc. So, I will show you a tricky way
to run scheduled jobs using pure ASP.NET without requiring any Windows service.
This solution runs on any hosting service providing just ASP.NET hosting. As a
result, you can have the scheduled job feature in your ASP.NET web projects
without buying dedicated servers.

How it works

First we need
something in ASP.NET that is continuously running and gives us a callback. The
web server IIS is continuously running. So, we somehow need to get a frequent
callback from it so that we can lookup a job queue and see if there’s something
that needs to be executed. Now, there are several ways a web server comes to
us:

  • When a page hits

  • When an application starts

  • When an application stops

  • When a session starts and ends/timeouts

  • When a cache item expires

The page hit is
random. If no body visits your website for hours, you can’t do the pending jobs
for hours. Besides, the execution of a request is very short and needs to
finish as soon as possible. If you plan to execute scheduled jobs on page
execution, then the page will take longer to execute which will result in a
poor user experience. So, clearly this is not an option.

When an
application starts, we get a callback in the
 Application_Start method of Global.asax. So, this is a good place to start a
background thread which runs forever and executes the scheduled jobs. However,
the thread can be killed anytime the web server decides to take a nap due to
zero load.

When an
application stops, we get a callback at
 Application_End. But we can’t do anything here because the whole application is going to
die soon.

Session_Start in Global.asax is triggered when a user visits a page that requires a new session to be
initiated. So, this is also random. We need something that consistently and
periodically fires.

A cache item
expires on a given time or duration. In ASP.NET, you can add entries in the
 Cache and set an absolute expiry date time or you can set a duration after which
the item is removed from the cache. You can do this by utilizing the following
method of the
 Cache class:

public void Insert ( System.String key , System.Object value ,

                    
System.Web.Caching.CacheDependency dependencies ,

                     System.DateTime
absoluteExpiration ,

                     System.TimeSpan
slidingExpiration ,

                    
System.Web.Caching.CacheItemPriority priority ,

                    
System.Web.Caching.CacheItemRemovedCallback onRemoveCallback )

The onRemoveCallback is a delegate to a method which is called whenever a cache item expires.
In that method we can do anything we like. So, this is a good candidate for
running code periodically, consistently without requiring any page visit.

This means, we can
simulate a Windows Service utilizing Cache timeout! Now who thought that this
would be possible?

Creating cache item callbacks

First on Application_Start we need to register a cache item that will expire in 2 minutes. Please
note, the minimum duration you can set for expire callback is 2 minutes.
Although you can set a lower value, it does not work. Most probably the ASP.NET
worker process looks at the cache items once every two minutes.

private const string DummyCacheItemKey = "GagaGuguGigi";

 

protected void Application_Start(Object sender, EventArgs e)

{

    RegisterCacheEntry();

}

 

private bool RegisterCacheEntry()

{

    if( null != HttpContext.Current.Cache[ DummyCacheItemKey ] ) return false;

 

    HttpContext.Current.Cache.Add(
DummyCacheItemKey,
"Test", null,

        DateTime.MaxValue,
TimeSpan.FromMinutes(
1),

        CacheItemPriority.Normal,

        new CacheItemRemovedCallback( CacheItemRemovedCallback ) );

 

    return true;

}

This cache entry
is a dummy entry. We do not store any valuable information here because
whatever we store here, might be gone on application restart. Besides all we
need is the frequent callback from this item.

Inside the
callback, we do all the service work:

public void CacheItemRemovedCallback( string key,

            object value, CacheItemRemovedReason reason)

{

    Debug.WriteLine("Cache item callback: " + DateTime.Now.ToString() );

 

    // Do the service works

 

    DoWork();

}

Store item in cache again upon expire

Whenever the cache
item expires, we get a callback and the item is gone from the cache. So, we no
longer get any callback in future. In order to have a continuous supply of
callback, we need to store an item in cache again upon expiration. This seems
quite easy; we can call the
 RegisterCacheEntry function shown above from the callback function, isn’t it? It does not
work. When the callback method fires, there is no
 HttpContext available. TheHttpContext object is only available when a request is being processed. As the
callback is fired from the web server behind the scene, there is no request
being processed and thus no
 HttpContext is available. As a result, you cannot get access to the Cache object from the callback function.

The solution is,
we need to simulate a request. We can make a dummy call to a dummy webpage by
utilizing the
WebClient class in the .NET framework. When the dummy page is being executed, we can
get hold of the
HttpContext and then register the callback item again.

So, the callback
method is modified a bit to make the dummy call:

public void CacheItemRemovedCallback( string key,

            object value, CacheItemRemovedReason reason)

{

    Debug.WriteLine("Cache item callback: " + DateTime.Now.ToString() );

 

    HitPage();

 

    // Do the service works

 

    DoWork();

}

The HitPage function makes a call to a dummy page:

private const string DummyPageUrl =

    "http://localhost/TestCacheTimeout/WebForm1.aspx";

 

private void HitPage()

{

    WebClient client = new WebClient();

   
client.DownloadData(DummyPageUrl);

}

Whenever the dummy
page executes, the
 Application_BeginRequest method gets called. There we can check whether this is a dummy page
request or not.

protected void Application_BeginRequest(Object-->

作者:

抱歉!评论已关闭.