Looks that Kill: Componentes de DevExpress y ThreadAbortException
Aviso a Navegantes: El siguiente post no va a ser políticamente correcto. Es más, creo que este año los chicos de Developer Express Inc. no me van a enviar un jamoncito ni una botella de vino por Navidad, precisamente. Y sin embargo, me siento en la obligación de compartir esto con vosotros… así que ¡allá vamos!
Últimamente he pasado bastante tiempo en tierras Navarras: alegrándome la vista con el verdecito que ya voy echando tanto de menos, disfrutando como un enano de los increibles pintxos de Pamplona y, de cuando en cuando, tirando de WinDbg para resolver algunos problemas de rendimiento que tiene uno de nuestros clientes aquí :)
Y es que en estas semanas he visto muchos escenarios interesantes: las tradicionales fugas de memoria por manejadores de eventos en páginas ASP.NET, problemas con el tamaño de sesión y el histórico de ViewStates, un curioso caso de excepciones indeseadas en cada invocación a un Servicio Web con transacciones que espero poder postear en breve… vamos, ¡que no me he aburrido!
Uno de estos problemas, y uno de los más llamativos, tenía que ver con la increible cantidad de excepciones lanzadas en el frontal web de la aplicación; estamos hablando de varias decenas de miles a la hora! A estas alturas no hace falta que os diga que el mero hecho de lanzar una excepción (aunque esta sea capturada por nuestro código) supone un impacto negativo en el rendimiento, y debemos tratar de evitar estas situaciones.
Si bien el contador de rendimiento # of Excepts Thrown en .NET CLR Exceptions era suficiente para detectar el problema, necesitabamos algo más para averiguar el tipo de excepiones lanzadas y poder investigar mejor el problema real. Como no podía ser de otra manera, ese algo más fue WinDbg :)
Después de adjuntarme al proceso, le pedí a WinDbg que se detuviera cada vez que saltara una excepcion .NET (sxe clr) y que en cada una de ellas, me mostrara la información de la excepción (!pe), la pila de llamadas administrada (!clrstack) y que prosiguiera la ejecución sin manejar la excepción (gn):
sxe –c “!pe; !clrstack; gn” clr
La salida demostró que había dos tipos que conformaban la práctica totalidad de las excepciones:
- MissingManifestResourceException:
Esta excepción no me preocupaba demasiado, porque en número era muy inferior a la que veremos a continuación. Sin embargo, es curioso el hecho de que siempre sucedía en unas DLLs de DevExpress.
A continuación os pongo una ocurrencia del problema (omitiendo la pila de llamadas, que en este caso no es relevante):
(d18.11b4): CLR exception - code e0434f4d (first chance)
Missing image name, possible paged-out or corrupt data.
Exception object: 0000000240991ed8
Exception type: System.Resources.MissingManifestResourceException
Message: Could not find any resources appropriate for the specified culture or the neutral culture. Make sure "Resources.DevExpress_XtraScheduler_v9_3_Core.resources" was correctly embedded or linked into assembly "App_GlobalResources.2gule58b" at compile time, or that all the satellite assemblies required are loadable and fully signed.
InnerException: <none>
StackTrace (generated):
<none>
StackTraceString: <none>
HResult: 80131532
Después de revisar la solución, comprobamos que todas las referencias a todas las DLLs de DevExpress estaban aparentemente bien, por lo que decidimos involucrar al soporte de Developer Express en este caso.
Esta excepción ya suena peor ¿verdad? Hacía no mucho, en este mismo cliente, nos habíamos encontrado con el conocido bug de la plataforma en el método Response.End(), pero…. ¡no adelantemos acontecimientos!
Primero veamos un ejemplo de una de las excepciones que capturamos y su volcado de pila:
(d18.11b4): CLR exception - code e0434f4d (first chance)
Exception object: 0000000200f83940
Exception type: System.Threading.ThreadAbortException
Message: Thread was being aborted.
InnerException: <none>
StackTrace (generated):
SP IP Function
000000000911E8E0 0000000000000001 System.Threading.Thread.AbortInternal()
000000000911E8E0 000006427834300A System.Threading.Thread.Abort(System.Object)
000000000911E930 00000642BC9131D6 System.Web.HttpResponse.End()
000000000911E980 0000064281AC2EA6 DevExpress.Web.ASPxClasses.Internal.HttpUtils.EndRespons
StackTraceString: <none>
HResult: 80131530
OS Thread Id: 0x11b4 (23)
Child-SP RetAddr Call Site
000000000911e980 00000642803d1965 DevExpress.Web.ASPxClasses.Internal.HttpUtils.EndResponse()
000000000911e9c0 00000642803d049d DevExpress.Web.ASPxClasses.Internal.ResourceManager.ProcessRequest()
000000000911ea80 00000642bc8f2eb0 DevExpress.Web.ASPxClasses.ASPxHttpHandlerModule.BeginRequestHandler(System.Object, System.EventArgs)
000000000911eab0 00000642bc8e449b System.Web.HttpApplication+SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
000000000911eb10 00000642bc8f2215 System.Web.HttpApplication.ExecuteStep(IExecutionStep, Boolean ByRef)
000000000911ebb0 00000642bc8e3553 System.Web.HttpApplication+ApplicationStepManager.ResumeSteps(System.Exception)
000000000911ec60 00000642bc8e7874 System.Web.HttpApplication.System.Web.IHttpAsyncHandler.BeginProcessRequest(System.Web.HttpContext, System.AsyncCallback, System.Object)
000000000911ecc0 00000642bc8e745c System.Web.HttpRuntime.ProcessRequestInternal(System.Web.HttpWorkerRequest)
000000000911ed50 00000642bc8e608c System.Web.HttpRuntime.ProcessRequestNoDemand(System.Web.HttpWorkerRequest)
000000000911ed90 000006427f602012 System.Web.Hosting.ISAPIRuntime.ProcessRequest(IntPtr, Int32)
Como se puede ver, uno de los métodos de las librerias de DevExpress, DevExpress.Web.ASPxClasses.Internal.HttpUtils.EndResponse está invocando a System.Web.HttpResponse.End(), el cual a su vez lanza una excepción de tipo ThreadAbortException.
Mmm… antes hablabamos de un caso conocido de excepción ThreadAbortException en las llamadas a HttpResponse.End(), así que una rápida busqueda nos lleva al KB312629, que si no conoceis os recomiendo que leais y comprobéis si vuestras aplicaciones se ven afectadas por él.
Una comprobación del código fuente del método de DevExpress (con la siempre inestimable ayuda de .NET Reflector), nos revela que los desarrolladores del producto son conscientes del problema, implementando un catch para el ThreadAbortException:
public static void EndResponse()
{
HttpContext.Current.ApplicationInstance.CompleteRequest();
try
{
GetResponse().End();
}
catch (ThreadAbortException)
{
}
}
Sin embargo, esta solución no evita el lanzamiento descontrolado de las excepciones que, como ya se ha comentado, a pesar de ser controladas, siguen teniendo un impacto sobre el rendimiento de la aplicación, ¡por no hablar del ruido que meten a la hora del monitorizar el sistema!
Con esta información en la mano, elaboré un documento detallado (mucho más que este post) para que mi cliente pudiera abrir un caso de soporte con la gente de DevExpress, esperando que resolvieran el problema que nos estaba impactando severamente en producción, mediante la implementación condicional de una llamada a HttpContext.Current.ApplicationInstance.CompleteRequest() en lugar de invocar al método HttpResponse.End().
La respuesta de la gente de DevExpress no se hizo esperar mucho (punto para ellos!), y se resume en lo siguiente:
- No van a cambiar el código, ni a dejar la opcion al desarrollador de asumir el cambio con el CompleteRequest.
- Del problema de las MissingManifestResourceException simplemente nos comentaron que esas excepciones son esperadas y que no nos preocupemos.
- Con las dos respuestas (a todas luces insuficientes y debatibles) llegó la notificación del cierre del caso. Sin derecho a réplica.
En esta ocasion, lo que más rabia me da no es la parte técnica; puedo entender los problemas que tengan para realizar el cambio (aunque no es de recibo que su código levante tantas excepciones), hasta casi puedo entender que su código levanta excepciones buscando recursos localizados inexistentes… pero por lo que no paso es por la dejadez y el mal trato al usuario en el soporte técnico.
Asi que… ¡Avisados estais! :) Vamos a por el mini-resumen del día…
Resumiendo:
- ¿Habéis monitorizado su vuestra aplicacion tira excepciones? Si no es así, revisad los contadores de rendimiento y tirad de WinDbg… ¡a lo mejor os llevais una sorpresa!
- Comprobad si teneis ThreadAbortException, y si es así, evaluad el workaround descrito en el KB.
- ¿Estais planteandoos adquirir una licencia para controles de usuario para un nuevo proyecto? ¡Aseguraos de tener un buen soporte, no sabeis lo útil que puede llegar a ser!
Keep Rockin’!
Rock Tip:
No podía ser de otra manera, este post solo lo podían presentar los chicos de Mötley Crüe con su “Looks that Kill”. Apropiado, porque en este caso la vistosa interfaz de usuario de la aplicación es la que contiene un pequeño veneno, como hemos visto ;)