asp.net.netiishttpmodule

Thread-pool friendly approach to Sleep?


I want to insert a sleep (aka throttle, delay, tarpit, dwell) in an ASP.net application (imagine something like failed logon attempt escalating delay).

protected void Page_Load(object sender, EventArgs e)
{
    Int32 sleepyTime = GetSleepyTime(Request);

    if (sleepyTime > 0)
        System.Threading.Thread.Sleep(sleepyTime);


    //...Continue normal processing
}

I want all remaining processing to continue as normal; i just want the user agent to suffer.

The problem is that ASP.net uses a ThreadPool to handle requests. If i were to Sleep for 5, 10, 30 seconds, i would be eating up a valuable limited resource.

I assume it needs to be something like:

protected void Page_Load(object sender, EventArgs e)
{
    Int32 sleepyTime = GetSleepyTime(Request);

    if (sleepyTime > 0)
       ABetterKindOfSleep(sleepyTime);

    //...Continue normal processing
}

private void ABetterKindOfSleep(int milliseconds)
{
   await SleepAsync(milliseconds);
}

private async void SleepAsync(int milliseconds)
{
   System.Threading.Thread.Sleep(milliseconds);
}

But never having written any async/await code, and not understanding the logic behind where the async and await go, or why, or even if it can be used to run async code: i don't know if it can be used to run async code.

Bonus Reading


Solution

  • It's easy enough.

    First you create an IHttpModule class:

    class TarpitHttpModule : IHttpModule
    {
    }
    

    And then you let IIS know about this module by registering it in web.config:

    <configuration>
       <system.webServer>
          <modules runAllManagedModulesForAllRequests="true">
             <add name="Tarpit" type="TarpitHttpModule"/>
    

    If you are Cassini, add it to:

    <configuration>
       <system.web>
          <httpModules>
             <add name="Tarpit" type="TarpitHttpModule"/>
    

    Whenever an http request comes in, IIS will call your .Init method. This is where you will register your async event handler using:

    Code:

    public void Init(HttpApplication application)
    {
        //This is the synchronous event handler; which we don't want
        //application.BeginRequest += new EventHandler(this.Application_BeginRequest);
    
        //EventHandlerTaskAsyncHelper requires .NET 4.5
        //https://brockallen.com/2013/07/27/implementing-async-http-modules-in-asp-net-using-tpls-task-api/ 
        //  Archive: http://archive.is/Cdvle
        //
        //Normally you'd have to write a pair of methods:
        //    application.AddOnBeginRequestAsync(OnBegin, OnEnd);
        //
        //and then we'd have to write an OnBegin which returns IAsyncResult, and then OnEnd which takes the IAsyncResult.
        //The modern way is to use Tasks, and use the IAsyncResult that a Task **is**.
        //Fortunately the .NET team wrote a handy class that wraps up the boilerplate catching faults, etc,
        //and created the EventHandlerTaskAsyncHelper class
    
        var beginTaskHelper = new EventHandlerTaskAsyncHelper(BeginRequestAsync);
        application.AddOnBeginRequestAsync(beginTaskHelper.BeginEventHandler, beginTaskHelper.EndEventHandler);
    }
    

    So now we have to supply the BeginRequestAsync asynchronous handler:

    async Task BeginRequestAsync(object sender, EventArgs e)
    {
        var application = (HttpApplication)sender;
        var context = application.Context;
    
        // In reality i would use the context.Request to come up with a unique key 
        // for this user agent e.g. 
        String key = SHA256(UserHostAddress+UserAgent+AcceptTypes+UserLanguages).ToBase64();
        // And use that as a cache key store information about this user agent
        Object tarpitInfo = context.Cache.Get(agentIdentity);
        if (ti == null)
            return;
    
        // But in this SO demo, i'm just going to unconditionally sleep
        Boolean waitPerformed = await PerformDelay(context, tarpitInfo);
        if (waitPerformed)
        {
            context.Response.StatusCode = 429;
            context.Response.StatusDescription = "Too Many Requests";
            context.Response.End();
            return;
        }
    }
    

    And then the work of sleeping:

    async Task<Boolean> PerformDelay(HttpContext context, TarInfo ti)
    {
        int delayMs = 3000;
        Task delay = Task.Delay(delayMs);
        await delay;
        return true;
    }