
31/01/2008 02:56 por
zorry
Bueno, pues hemos conseguido averiguar y solucionar los problemas que teníamos con nuestra aplicación.
Al estresar nuestra aplicación, con 20 usuarios concurrentes, habíamos encontrado que sólo un 10% de las pruebas conseguían finalizar correctamente. Con lo que nos ponemos con la ingeniería forense:
- Comprobamos que nuestra aplicación web no sea el causante de los problemas. En principio, no lo parece. El consumo de CPU es del orden del 2% y la de memoria algo alta, pero nuestra aplicación siempre ha sido un poco "hambrienta", unos 100 megas. Pero la máquina debería ser capaz de aguantar la carga de proceso sin problemas.
- Descartado nuestro frontal, vamos a comprobar cómo va el servidor de CRM. De nuevo, parece que todo va como debería, muy poco consumo de CPU (2%) y memoria otra vez contenido (aproximadamente 100 megas también).
- Descartamos el CRM, con lo que nos queda inspeccionar el servidor de SQL Server. Entramos en la máquina y ya notamos que hay algo que no va todo lo bien que debiera: La CPU está en torno al 98% de carga (de un quad core) y la memoria está disparada, aproximadamente tiene unos 6 Gb de memoria el servicio de SQL. Una vez paramos las pruebas de carga, el consumo de CPU baja al 0%. Parece que tenemos fichado al culpable. Ahora vamos a ver cómo lo solucionamos.
Decidimos volver a lanzar las pruebas de carga, pero esta vez con un SQL Profiler, de manera que podamos trazar las consultas realizadas al servidor SQL. Tras unos minutos capturando tráfico, vemos que hay algunas consultas que saturan la CPU. Con lo que vamos a ver de que manera las optimizamos.
Cargamos Database Engine Tuning Advisor, una herramienta de SQL Server 2005, e introducimos el archivo de traza capturado antes. Le decimos que tiene que inspeccionar la base de datos de CRM, y se pone a la tarea. Tras un rato ejecutando y analizando nuestro conjunto de pruebas, nos sugiere crear una serie de índices y estadísticas sobre bastantes tablas, estimando la mejora en un 90% aproximado. Posteriormente la herramienta nos permite aplicar o salvar los cambios, y elegimos salvarlos en un archivo sql para poder aplicarlo en todos los entornos.
Pero nos queda la preocupación de si será recomendable modificar la base de datos de CRM, ya que no tenemos control sobre ella, al estar la estructura de esta base de datos mantenida por el propio CRM. Pero tras investigar un poco, encontramos en este artículo lo que necesitábamos: Está soportado crear índices siempre y cuando no sean índices únicos.
Así que aplicamos los cambios sobre la estructura de base de datos, y volvemos a realizar la prueba. Y los resultados no pueden ser más esperanzadores, ya obtenemos más de un 90% de pruebas correctas, y ahora el servidor es capaz de procesar más del doble de peticiones http por minuto!
299bf008-fd84-4c0e-9fbb-5bf3b89e48f4|0|.0

18/01/2008 11:26 por
zorry
Como bien sabéis, el ViewState se almacena en un campo hidden en las páginas, de manera que el navegador cliente es el que recibe y envía todos los datos del ViewState, persistiendo el estado de la página entre PostBacks.
Este escenario es el más aceptable en líneas generales. Pero si tenemos poco ancho de banda, podríamos cambiar el lugar donde almacenar el ViewState y almacenarlo en la sesión. Para ello, en la página que queramos configurar su ViewState sólo tenemos que implementar el siguiente código:
1: protected override PageStatePersister PageStatePersister
2: {
3: get
4: {
5: return new SessionPageStatePersister(this);
6: }
7: }
De todos modos, es necesario tener cuidado y valorar bien cuántos usuarios ejecutarán nuestra aplicación y cuantos datos se almacenan en sesión, puesto que es posible que si nuestra aplicación albergará muchos usuarios y se guardan muchos datos en sesión, se sobrecargue en exceso el servidor web.
7e1bccaa-448b-4786-b023-66b4a9a79b9b|0|.0

09/01/2008 09:31 por
zorry
La aplicación web en la que estoy trabajando, trabaja en modo cookieless. Necesito probarla y replicar el funcionamiento del navegador. Una aplicación funcionando en modo cookieless almacena el ID de sesión en la URL, de manera que tras la primera petición del navegador, la aplicación redirige a otra Url, con el ID de sesión embebido, y las consecuentes peticiones contienen este Id de Sesión.
Cuando realizamos una captura de sesión mediante Fiddler (ver artículo anterior), al capturar la sesión, capturamos el SessionID, quedando este en hardcode en las peticiones. Si necesitamos que el SessionID varíe para cada prueba, necesitamos que capturar el SessionID de la primera petición y propagarlo a las siguientes.
Para poder hacerlo, vamos a crearnos un CustomExtractionRule, para ello, nos creamos una clase que herede de ExtractionRule:
1: using System;
2: using System.Collections.Generic;
3: using System.Text;
4: using Microsoft.VisualStudio.TestTools.WebTesting;
5:
6: namespace WebTest.Funcional
7: {
8: public class ExtractCookielessSessionId : ExtractionRule
9: {
10: public override void Extract(object sender, ExtractionEventArgs e)
11: {
12: if (!e.Response.IsHtml)
13: {
14: e.Success = false;
15: e.Message = "The response did not contain HTML";
16: }
17:
18: //Obtiene el SessionID para cookieless
19: string beginSessionId = "(S(";
20: string endSessionId = "))";
21: string url = e.Response.ResponseUri.AbsoluteUri;
22: int inicioSID = url.IndexOf(beginSessionId);
23: int finSID = url.IndexOf(endSessionId);
24:
25: if (inicioSID >= 0 && finSID > inicioSID)
26: {
27: string sessionId = url.Substring(inicioSID, finSID - inicioSID + endSessionId.Length);
28: e.WebTest.Context.Add(this.ContextParameterName, sessionId);
29: }
30:
31: }
32:
33: public override string RuleName
34: {
35: get { return "ExtractCookielessSessionId"; }
36: }
37:
38: public override string RuleDescription
39: {
40: get { return "Extracts Cookieless Session Id"; }
41: }
42: }
43: }
Una vez creada esta clase, compilamos el proyecto de pruebas, y agregamos la nueva CustomExtractionRule a la primera petición. Además, a la regla, le ponemos el nombre de parámetro de contexto SESSION (de esta manera, la regla de extracción sacará el SessionID a esta variable de contexto).

Posteriormente, sólo nos quedará modificar el resto de las Url, sustituyendo el SessionID por el valor {{SESSION}}:

Con esto, al ejecutar el webtest podremos ver cómo se emplea cada vez un SessionID diferente, con lo que conseguimos independizar cada prueba.
3693d3a6-e3d4-487e-bfbc-00945fd18e30|0|.0

05/01/2008 06:55 por
zorry
En el proyecto en el que trabajo actualmente, tenemos que probar cómo se comporta la aplicación web bajo carga de muchos usuarios. Para realizar la carga, empleo Visual Studio 2005 Team Suite.
En una primera aproximación para realizar las pruebas de carga, realicé una captura web mediante la herramienta nativa de Visual Studio. En principio funciona bien, captura las peticiones http a la aplicación, pero como nuestra aplicación funciona con Ajax, las peticiones que realiza la aplicación de manera asíncrona mediante Javascript no son capturadas, con lo que las pruebas no son completas.
La solución que he encontrado ha sido emplear Fiddler para capturar el tráfico. Se arranca esta herramienta de captura antes de arrancar la aplicación web, se realiza la prueba de navegación desde un navegador, y posteriormente, Fiddler permite salvar todo el tráfico entre nuestro navegador y la aplicación web (incluido Ajax) como prueba de Visual Studio.
Posteriormente, este archivo webtest puede importarse en un proyecto de pruebas de Visual Studio 2005 Team suite para realizar las pruebas de carga.
51acd91e-f777-40f4-a0a7-fe91b2742ca8|0|.0

29/12/2007 08:12 por
zorry
Revisando el fix que hice ayer, y debido sobre todo a que no me gusta hacer modificaciones en librerías que no dependen de mí (sobre todo, para evitar que en una nueva release del AjaxControlToolkit me machaquen los cambios), me he fijado en una propiedad del ToolkitScriptManager denominada CombineScriptsHandlerUrl. Esta propiedad permite especificar un handler específico para manejar la combinación de todos los script del AjaxControlToolkit. De modo que me dispongo a deshacer los cambios que hice ayer, y hago varios cambios en mi aplicación web:
- En la definición del ToolkitScriptManager, he incluído el siguiente atributo: CombineScriptsHandlerUrl="~/CombineScriptsHandler.ashx"
- He creado un nuevo handler en la aplicación con el nombre definido en el nombre anterior. En el archivo ashx he introducido el siguiente código:
1: <%@ WebHandler Language="C#" Class="CombineScriptsHandler" %>
2:
3: using System;
4: using System.Web;
5: using AjaxControlToolkit;
6:
7: public class CombineScriptsHandler : IHttpHandler
8: {
9: /// <summary>
10: /// ProcessRequest implementation outputs the combined script file
11: /// </summary>
12: /// <param name="context"></param>
13: public void ProcessRequest(HttpContext context)
14: {
15: if (!ToolkitScriptManager.OutputCombinedScriptFile(context))
16: {
17: throw new InvalidOperationException("Combined script file output failed unexpectedly.");
18: }
19: }
20:
21: /// <summary>
22: /// IsReusable implementation returns true since this class is stateless
23: /// </summary>
24: public bool IsReusable
25: {
26: get { return true; }
27: }
28: }
29:
Con estos cambios por fin he conseguido que la aplicación funcione correctamente en modo cookieless, y sin los problemas de javascript que me estaba encontrando anteriormente.
82354f83-f178-464d-8e98-09ec589bb2f2|0|.0

28/12/2007 15:12 por
zorry
Ok, por fin tengo mi aplicación funcionando en modo cookieless (para que funcione ciertas cosas de la aplicación que la consume). Instalo la última version del AjaxControlToolkit, configuro el tag ToolkitScriptManager correctamente, y pongo el tag CombineScripts a true, para que me combine todos los scripts generados por el toolkit en uno sólo.
Y al cargar la página, me pierde la sesión, en las trazas de IIS, veo que se está llamando a mi página inicial con otro SessionID, con lo que me da un error al cargar los scripts y la página no funciona...
En concreto el problema es que trata de cargar el script sin poner delante el SessionID:
/WebApp/Resumen.aspx?_TSM_HiddenField_=ctl00_scriptManager_HiddenField&_TSM_CombinedScripts_=%3b%3bAjaxControlToolkit%2c+Version%3d1.0.11119.32029%2c+Culture%3dneutral%2c+PublicKeyToken%3d28f01b0e84b6d53e%3aes-ES%3a2d550902-56f7-46bd-9795-b930029c9f3f%3ae2e86ef9%3a9ea3f0e2%3a9e8e87e9%3a1df13a87%3a80f47b59%3ad7738de7
Tenemos que lograr que el AjaxControlToolkit renderice la siguiente llamada para asegurarnos que no se pierde la sesión:
/WebApp/(S(w4fcmx45goyog355ydy4rw3w))/Resumen.aspx?_TSM_HiddenField_=ctl00_scriptManager_HiddenField&_TSM_CombinedScripts_=%3b%3bAjaxControlToolkit%2c+Version%3d1.0.11119.33116%2c+Culture%3dneutral%2c+PublicKeyToken%3d28f01b0e84b6d53e%3aes-ES%3af5528113-d4f3-4bcc-99aa-dc1a40d76a47%3ae2e86ef9%3a9ea3f0e2%3a9e8e87e9%3a1df13a87%3a80f47b59%3ad7738de7
Para ello, abrimos el código del AjaxControlToolkit... En concreto tocamos en la clase ToolkitScriptManager.cs... (Tengamos en cuenta que estoy trabajando con la release del 19 de Noviembre de 2007). La línea a modificar es la 144, y la reemplazaremos dejando el siguiente código:
144: _combinedScriptUrl = String.Format(CultureInfo.InvariantCulture,
145: "{0}?{1}={2}&{3}={4}", ((null != _combineScriptsHandlerUrl) ? _combineScriptsHandlerUrl.ToString()
146: : (new Uri(Page.Request.Url, Page.Request.RawUrl)).AbsolutePath)
147: , HiddenFieldParamName, HiddenFieldName, CombinedScriptsParamName,
148: HttpUtility.UrlEncode(SerializeScriptEntries(_scriptEntries, false)));
El código modificado se encuentra en la línea 146. De esta manera, se parsea la Url de la petición haciendo que se solicite la Url del script con el id de sesión, funcionando así correctamente la llamada Cookieless.
b3b7c0b1-e88d-463b-be94-01fe639b59ee|0|.0

19/12/2007 06:12 por
zorry
En la aplicación en la que estoy trabajando, estabamos teniendo un problema al llamar a un servicio web, publicado por Microsoft CRM 3.0. La aplicación estaba funcionando correctamente, en nuestras máquinas y la máquina de pruebas en desarrollo. Sin embargo, al instalar en el segundo entorno de pruebas de desarrollo (una máquina con Windows 2003 x64) la aplicación se moría, comenzaba a comer memoria y CPU hasta que había una excepción de timeout y no lográbamos pasar de ahí.
Tras meter trazas en la aplicación, logré aislar el fallo en una llamada a un servicio web de CRM. Una vez ahí, decidí realizar la misma llamada, pero aislandome del contexto de IIS, es decir, copié el código a una aplicación de consola con el mismo resultado: La aplicación en 32 bits funciona bien pero en x64 no.
Instalé Fiddler y Netmon y conseguí ver que ni siquiera se trataba de comunicar con el servidor. Cada vez estaba más claro, parecía un bug del Framework 2.0 para x64. Ahi un compi me preguntó si la máquina la teníamos a nivel de Service PAcks y demás y recordé que esta máquina nos la dieron sin actualizar y no la hemos actualizado en meses!!!
Así que manos a la obra, instalo todos los hotfixes pendientes (22) y el SP1 del Framework 2.0 para x64... Y mano de santo, oiga! Efectivamente había estado dándome de cabezazos contra un bug del Framework. Aunque he estado leyéndome toda la lista de fixes y no he visto que bug han arreglado para que todo me funcione correctamente...
Espero que no se me olvide en próximas ocasiones... Como recordatorio sirva este post.
ad98f46e-b5e9-47ad-a45a-53e4b86fe839|0|.0

05/12/2007 08:12 por
zorry
Al hilo del post anterior, la aplicación Windows que trabaja con nuestra aplicación web, es una aplicación con un Internet Explorer embebido. Como el manejo de las Url lo realiza por debajo, no soporta llamar a una Url con un SessionId específico de la siguiente forma:
http://servidor/Aplicacion/(S(i1hxwon1me1aazix1w1jnd55))/default.aspx
Esto es porque sólo soporta enviar parámetros mediante querystring, de la siguiente manera:
http://servidor/Aplicacion/default.aspx?SessionId=(S(i1hxwon1me1aazix1w1jnd55))/
Con lo que necesitamos realizar un reescrito de la Url para que el servidor coja correctamente el Id de sesión que queremos que emplee. Lo conseguimos mediante un modulo Http.
Lo primero es crearnos una clase dentro de nuestra aplicación web:
1: namespace MiAplicacion.Web.Modulo
2: {
3: public class RewriteModule : IHttpModule
4: {
5: public RewriteModule() { }
6:
7: public void Dispose() { }
8:
9: public void Init(HttpApplication application)
10: {
11: application.BeginRequest += new EventHandler(application_BeginRequest);
12: }
13:
14: void application_BeginRequest(object source, EventArgs e)
15: {
16: HttpApplication app = (HttpApplication)source;
17:
18: //Control de sesión cookieless
19: if (getIsCookielessSessionState())
20: processCookielessSessionId(app);
21: }
22: }
23: }
En este código nos creamos un manejador para el evento BeginRequest. En este manejador comprobamos que la aplicación tiene configurada la sesión cookieless (línea 19), y en el caso de que así sea, reescribir la url (línea 20).
Seguidamente, hay que modificar el web.config, para activar el módulo http:
1: <httpModules>
2: <add name="AuthHTTPModule" type="MiAplicacon.Web.Modulo.RewriteModule"/>
3: </httpModules>
Y por último, y no menos importante, el código para los métodos auxiliares:
1: private bool getIsCookielessSessionState()
2: {
3: object untypedSessionState = ConfigurationManager.GetSection("system.web/sessionState");
4: try
5: {
6: if (untypedSessionState != null)
7: {
8: System.Web.Configuration.SessionStateSection section =
9: (System.Web.Configuration.SessionStateSection)untypedSessionState;
10:
11: return section.Cookieless == HttpCookieMode.UseUri;
12: }
13: }
14: catch
15: {
16: return false;
17: }
18:
19: return false;
20: }
21:
22: private void processCookielessSessionId(HttpApplication app)
23: {
24: //Tratamiento para las sesiones cookieless
25: string parameterSessionId = "SessionId";
26: string url = app.Request.RawUrl;
27: int indexOfSessionId = url.IndexOf(parameterSessionId, StringComparison.CurrentCultureIgnoreCase);
28:
29: if (indexOfSessionId >= 0)
30: {
31: string sessionId = url.Substring(indexOfSessionId + parameterSessionId.Length + 1);
32: if (sessionId.IndexOf("&") >= 0)
33: sessionId = sessionId.Substring(0, sessionId.IndexOf("&"));
34:
35: if (sessionId.Length > 0)
36: {
37: url = url.Replace(app.Request.ApplicationPath, String.Empty);
38: url = url.Replace(parameterSessionId + "=" + sessionId, String.Empty);
39:
40: string newUrl = app.Request.ApplicationPath + "/" + sessionId + "/" + url;
41:
42: //Estandarizamos la url
43: newUrl = newUrl.Replace("?&", "?");
44: newUrl = newUrl.Replace("//", "/");
45:
46: if (newUrl[newUrl.Length - 1] == '?')
47: newUrl = newUrl.Remove(newUrl.Length - 1);
48: if (newUrl[newUrl.Length - 1] == '&')
49: newUrl = newUrl.Remove(newUrl.Length - 1);
50:
51: //Realizamos un redirect
52: app.Response.Redirect(newUrl);
53: }
54: }
55: }
En el primer método leemos el web.config para obtener si está activada la sesión cookieless (en el evento BeginRequest, no se puede acceder a los objetos de HttpApplication relativos a la sesión).
En el segundo método, parseamos la querystring y en el caso de que llegue el valor SessionId, realiza un Response.Redirect a la url con el SessionId embebido correctamente.
445cc567-d25f-405e-b5a3-944944e2c60e|0|.0

04/12/2007 21:12 por
zorry
En el proyecto en el que estoy trabajando, hemos tenido que securizar una transacción de datos mediante RSA. Este cifrado se emplea para asegurar que nadie puede obtener un usuario y contraseña que se envía entre una aplicación Windows Forms de cliente, y nuestra aplicación ASP.Net.
Si bien el cifrado se realiza sin problemas en la aplicación Windows Forms, cifrando con la clave pública, estaba teniendo problemas en el descifrado de los datos.
El código original era el siguiente:
1: RSACryptoServiceProvider rsaCryptoServiceProvider = new RSACryptoServiceProvider(dwKeySize);
2: rsaCryptoServiceProvider.FromXmlString(xmlString);
3: byte[] encryptedBytes = System.Web.HttpServerUtility.UrlTokenDecode(inputString);
4: byte[] clearBytes = rsaCryptoServiceProvider.Decrypt(encryptedBytes, false);
5: return Encoding.UTF8.GetString(clearBytes);
Este código me estaba elevando una excepción en la línea 2, al tratar de leer la configuración de un string con el Xml y la clave privada. El error era un tanto críptico: CryptographicException: The system cannot find the file specified.
Navegando por la red, encontré un blog en el que especifica que el usuario que corre la aplicación debería poder realizar esta operación atacando el store de la máquina, con lo que me quedó este código:
1: CspParameters csp = new CspParameters();
2: csp.Flags = CspProviderFlags.UseMachineKeyStore;
3: RSACryptoServiceProvider rsaCryptoServiceProvider = new RSACryptoServiceProvider(dwKeySize, csp);
4: rsaCryptoServiceProvider.FromXmlString(xmlString);
5: byte[] encryptedBytes = System.Web.HttpServerUtility.UrlTokenDecode(inputString);
6: byte[] clearBytes = rsaCryptoServiceProvider.Decrypt(encryptedBytes, false);
7: return Encoding.UTF8.GetString(clearBytes);
Esto es, añadiendo las dos primeras líneas y pasando los parámetros del CSP al constructor del Provider.
No obstante, los problemas no han acabado aquí. Como el resto del proceso corre impersonado, al parecer el GC da problemas al tratar de eliminar la clave privada del store. Aproximadamente al minuto de la ejecución del descifrado, se elevaba una excepcion CryptographicException: Keyset not found y se moría el proceso w3wp... Maldita sea...
Tras más investigaciones, logré averiguar que el GC trata de eliminar la clave del almacén, pero con las credenciales de otro usuario, por lo que no encuentra la clave y el proceso de IIS se muere.
Pero al final logré arreglarlo mediante el siguiente código:
1: CspParameters csp = new CspParameters();
2: csp.Flags = CspProviderFlags.UseMachineKeyStore;
3: RSACryptoServiceProvider rsaCryptoServiceProvider = new RSACryptoServiceProvider(dwKeySize, csp);
4: rsaCryptoServiceProvider.FromXmlString(xmlString);
5: byte[] encryptedBytes = System.Web.HttpServerUtility.UrlTokenDecode(inputString);
6: byte[] clearBytes = rsaCryptoServiceProvider.Decrypt(encryptedBytes, false);
7: rsaCryptoServiceProvider.Clear();
8: return Encoding.UTF8.GetString(clearBytes);
Esto es, insertando la línea 7. Esta línea elimina la clave del almacén de manera explícita, con lo que el GC no tiene que hacerlo con posterioridad y el proceso finaliza correctamente... Por fin y tras dos días pegándome con ello :-(
8a6b1ae0-4549-4764-a1bd-beab150c9f6a|0|.0

03/06/2007 19:06 por
zorry
Bueno, tras una gran pausa, sigamos con este pequeño tutorial acerca de cómo usar las extensiones Ajax en .Net.
Partimos de una versión modificada de la demo del artículo anterior. Recordemos que en el artículo anterior vimos como realizar actualizaciones parciales de la página mediante el control UpdatePanel, evitando realizar postbacks completos del cliente al servidor, evitando el redibujado completo del navegador en cada cambio de la página.
En esta demo, en lugar de realizar la carga de un DropDownList desde otro, lo que hacemos es realizar la carga de un GridView dependiendo de la selección desde un DropDownList:
Lo que ocurrirá es que el usuario no sabrá si debe esperar para trabajar con la página, si la carga del grid inferior tarda más de lo normal (por ejemplo, si la base de datos está sobrecargada, o la consulta es compleja o trae muchos datos). Para ello, emplearemos el control UpdateProgress que nos facilita las extensiones Ajax. Para ello, en la página maestra, introduciremos el siguiente código:
<asp:UpdateProgress ID="UpdateProgress1" runat="server" >
<ProgressTemplate>
<div class="progress">
<asp:Image ID="Image1" runat="server"
ImageUrl="~/images/indicator_mozilla_blu.gif"/>
Actualizando página
</div>
</ProgressTemplate>
</asp:UpdateProgress>
Mediante este control, conseguimos que el HTML que está dentro del ProgressTemplate se muestre cuando las extensiones Ajax detecten que hay una llamada asíncrona al servidor. Dentro del template se puede poner cualquier código HTML ya que en cliente se renderiza con un DIV oculto. En este caso, estoy mostrando un DIV con una imagen animada y un texto indicando al usuario que la página se está actualizando. Este DIV tira de una clase llamada progress, que contiene cualquier decoración CSS que se nos ocurra. En nuestro caso, quería que se mostrara sobre los controles que están siendo actualizados, por lo que la clase progress queda en el ejemplo asi:
.progress
{
border-right: #ff6633 thin solid;
border-top: #ff6633 thin solid;
vertical-align: middle;
border-left: #ff6633 thin solid;
border-bottom: #ff6633 thin solid;
position: absolute;
background-color: #ffcc66;
text-align: center;
padding: 10px;
left: 100px; top: 100px;
}
Tras aplicar esto, y cambiar de selección en el DropDownList, podremos ver el siguiente efecto:
Esto es todo, hasta la siguiente entrega del tutorial.
f7a21ad3-47d5-42eb-8dba-be6c73120a94|0|.0