Porque no se reflejan inmediatamente los cambios en aplicaciones ASP.NET

Esta es una problemática relativamente común. Por ejemplo, actualizamos un ensamblado (DLL) de nuestra aplicación ASP.NET, y cuando la probamos vemos que todavía no se han reflejado los cambios y que la aplicación se comporta igual que antes del cambio. ¿Por qué no se reflejan los cambios inmediatamente?

ASP.NET trabaja con application domains para proporcionar aislamiento entre aplicaciones alojadas en un mismo proceso. El application domain contiene todos los datos relacionados con una aplicación en particular, que son (simplificando): todos los ensamblados específicos de la aplicación, un objeto HttpRuntime y un objeto Cache. Un application domain se puede descargar del proceso que lo aloja, pero un ensamblado no se puede descargar del application domain que lo ha cargado.

Por lo tanto, cuando el runtime de .NET (CLR) detecta, por ejemplo, que algún ensamblado de la carpeta ~/bin de la aplicación ha sido modificado, carga un nuevo application domain con la última versión del ensamblado, y el application domain antiguo se descarga del proceso en cuanto haya terminado de servir todas las peticiones en curso.

Cada vez que se reinicia el application domain, todos los ensamblados tienen que volver a cargarse (previamente haciendo el shadow copy), el código vuelve a compilarse por el JIT-compiler, y la cache y las variables de sesión se reinician. El proceso de reinicio del application domain es bastante costoso, por lo que conviene evitar que se produzca cuando la aplicación web tiene mucha carga de usuarios.

ASP.NET utiliza la API Win32 ReadDirectoryChangesW para monitorizar cambios en carpetas y ficheros. Cuando detecta alguno de los siguientes cambios, se reinicia el application domain (esta lista la he sacado directamente del siguiente post de mi compañera Tess):

· Modificaciones en Web.Config, Machine.Config o Glabal.asax

· Modificaciones en la carpeta ~/bin o en su contenido

· La ruta física del directorio virtual es modificada

· Alguna subcarpeta de la aplicación web es eliminada (sólo en ASP.NET 2.0)

Adicionalmente, los siguientes eventos también provocan un reinicio del application domain:

· La política CAS (Code Access Security) es modificada.

· El número de recompilaciones dinámicas1 de ficheros *.aspx, *.ascx o *.asax excede el límite especificado en la propiedad <compilation numRecompilesBeforeAppRestart="15"/> en el machine.config o web.config (por defecto este valor es de 15)

1) Es importante puntualizar que modificar un fichero *.aspx, *.ascx o *.asax también implica una recompilación, dado que en ASP.NET absolutamente todo el código termina siendo compilado, incluso el código “HTML” de la capa de presentación. Las recompilaciones dinámicas, se gestionan de una forma distinta ya que en lugar de reiniciar el application domain directamente, se invalida el ensamblado con la versión antigua del fichero .aspx, y se carga un nuevo ensamblado con la nueva versión.

A la larga, dado que no se pueden descargar ensamblados de un application domain, este comportamiento de ASP.NET supondría que el consumo de memoria aumentaría progresivamente debido a la acumulación de ensamblados invalidados. Por este motivo, ASP.NET reinicia el application domain llegado un número determinado de recompilaciones, para de esta forma poder descargar los ensamblados invalidados.

Por lo tanto, para evitar los problemas asociados a los reinicios de los application domains y a las recompilaciones dinámicas, debemos seguir las siguientes medidas:

a) Realizar despliegues a producción cuando el servidor tenga la menor carga posible, o mejor incluso sacándolo del balanceo de forma que no atienda peticiones.

b) Cuando el punto a) no sea posible, podemos añadir / modificar los parámetros waitChangeNotification y maxWaitChangeNotification para minimizar el número de reinicios del application domain. Estos parámetros se pueden configurar en el web.config de la aplicación web en cuestión. Por ejemplo:

<system.web>

  <httpRuntime waitChangeNotification="10" maxWaitChangeNotification="60" />

</system.web>

Este mecanismo funciona de la siguiente manera: en el momento que sobrescribimos un fichero existente dentro de la estructura de carpetas de nuestra aplicación web, un objeto de File Change Notification detecta el cambio y transcurrido el tiempo que hayamos configurado en el campo waitChangeNotification reiniciará el application domain. Si durante este periodo de espera detecta otro cambio adicional, volverá a esperar otra vez el número de segundos que hayamos configurado en waitChangeNotification, y así sucesivamente hasta que se alcance el tiempo de espera que hayamos configurado en maxWaitChangeNotification. Cuando se alcanza el tiempo de espera configurado en maxWaitChangeNotification, el application domain se reinicia al margen de que se sigan detectando cambios.

De esta forma, el application domain no se reinicia hasta que se hayan “terminado de copiar” todos los nuevos ficheros a la carpeta de contenido, evitando así múltiples reinicios con su consiguiente coste. Esta problemática también está descrita en detalle en el post al que hacía referencia antes de mi compañera Tess Ferrandez.

c) En ocasiones es interesante probar si deshabilitando la “compilación por lotes” mejora el comportamiento. Este cambio ocasiona que no se compile la aplicación entera cuando se carga el application domain sino únicamente las partes relevantes de la aplicación requeridas para atender la primera petición. Por un lado se reduce el retraso inicial para responder a la primera petición, aunque por otro lado, cuando llegue una petición posterior que requiera una parte de la aplicación que no haya sido compilada, esa petición también experimentará un retraso.

Este cambio aplica sobre todo para aplicaciones compuestas por gran cantidad de ensamblados y páginas.

<system.web>

  <compilation batch="false" />

</system.web>

Espero que esta información os sea de utilidad.

- Daniel Mossberg