Objetivo
El objetivo es lanzar la un tarea en background desde el IIS de manera simple; evitando los costes y/o complejidades innecesiarias (Azure Worker Role, Windows Service…).
Propuesta
Hay un excelente articulo The Dangers of Implementing Recurring Background Tasks In ASP.NET de Phil Haack donde se describen los problemas asociados a las tareas en background lanzadas desde IIS.
El problema consiste básicamente en el parada de los proceso Web del IIS:
- Por modificación del Web.Config
- Reciclado del pool del IIS ya sea tras 29 horas o por inactividad del mismo
- Reinstalación de la aplicación
La propuesta de Phil Haack; se basa en registrar el job a los eventos del proceso de Web mediante HostingEnvironment.RegisterObject. Cuando nos registramos ante la parada somos notificados mediante el metodo Stop de la interfaz IRegisteredObject.
Existen dos problemas:
- El articulo propone que antes de la tarea periodica; se verifique que no se ha solicitado la parada del job para proceder en consecuencia. El problema existe cuando la tarea peridiodica tarda más de los 30 segundos que nos ofrece de timeout del método Stop. En este caso no funcionaria.
- Otro problema es el parada inesperada de la aplicación, ya sea por cuelgue, ejecución del comando IISReset, muerte del proceso IIS… En estos casos la aplicación no tiene notificación de este evento con lo que tampoco funcionaria.
Propuesta Modificada
Para solventarlo; la solución propuesta consiste en definir una acción de parada independientemente que la tarea periodica este en curso. Esto puede provocar problemas en la tarea periodica que deberíamos controlar; que igualmente ocurrirían. Pero podemos asegurarnos en la acción de parada de tomar las medidas oportunas para no dejar recursos bloqueados.
Además en el reinicio del JobHost se fuerza una parada del mismo; por si la aplicación se paro de forma inesperada (En este caso los recursos solo se liberarían al arrancar de nuevo la aplicación).
En el constructor de la clase JobHost; pasamos dos delegados: uno para abrir los recursos y otro para cerrarlos. El delegado de cierre se llamara automáticamente al parar el Pool Web del IIS y al reiniciar la aplicación.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Web.Hosting;
namespace Infrastructure.Job
{
public class JobHost : IRegisteredObject
{
private bool _shuttingDown;
private readonly Action _jobShuttdownAction;
public JobHost(Action jobStartAction, Action jobShuttdownAction)
{
HostingEnvironment.RegisterObject(this);
if (jobShuttdownAction != null) jobShuttdownAction();
if (jobStartAction!=null) jobStartAction();
_jobShuttdownAction = jobShuttdownAction;
}
public void Stop(bool immediate)
{
if (_shuttingDown) return;
_shuttingDown = true;
if (_jobShuttdownAction != null) _jobShuttdownAction();
HostingEnvironment.UnregisterObject(this);
}
public void Run(Action jobWorkAction, int sleepMilliseconds)
{
while (true)
{
if (_shuttingDown)
{
return;
}
try
{
jobWorkAction();
} catch {}
Thread.Sleep(sleepMilliseconds);
}
}
}
}
En Application_Start lanzamos nuestro Job con la clase JobHost
ThreadPool.QueueUserWorkItem(state => new JobHost(MyJob.Start,MyJob.Stop).Run(MyJob.DoWork, 100));
