lunes, 20 de junio de 2011

¿Y cuanto duran tus transacciones?

Mientras miraba de reojo un servidor de aplicación funcionando observo el mensaje "Begin transaction module bla bla..", le presto un poco de atención y me pongo en espera  del "Commit transaction.....". (Este servidor de aplicación va logueando en pantalla algunas de las tareas que va realizando).
Sucede entonces que los segundos pasan y nada....y pasan y nada, y de repente "Commit transaction". ¿Que paso acá? La base de datos no esta sobrecargada, yo la monitoreo, ¿Como puede tardar tanto una transacción ? Nos han enseñado una y otra vez que las transacciones deben ser cortas, tan cortas como sea posible. Cuando profundizo en el tema veo con horror los siguiente, esta transacción abarca entre 6 y 10 tablas y ante una situación anormal (Bastante improbable por suerte) la aplicación comienza a preguntar al usuario si quiere resolverlo de tal o cual manera, y mientras tanto la transacción abierta. Problema encontrado, problema solucionado y ahora acá viene el tema ¿Esta esto pasando en varios lugares o fue esta la única "patinada" de toda la aplicación? ¿Como saberlo?

En la entrada
http://pablosoligo.blogspot.com/2011/06/dbexpress-trace.html
se explico como llevar a una tabla de log las operaciones que realiza DBExpress con la base de datos, entre esas operaciones están los begin transaction/commit/rollback entre otras muchas cosas. ¿Y si sacamos la diferencia de tiempo entre un begin transaction y su correspondiente commit/rollback? podriamos ver si hay irregularidades, incluso podemos ordenar la cosa y saber cuales son las que mas demoran.
Desgraciadamente quien les escribe no sabe ni investigo como pedirle al DBExpress o al sqlserver el numero de transaction si es que existiera, tampoco tenemos en la tabla un campo que una un begin transaction con su commit/rollback, por tanto tendremos que usar las neuronas un poco mas.
La siguiente consulta es la base sql que podría resolver el misterio,



select DateDiff(millisecond, s1.FechaHora, s2.FechaHora) as 'Demora', *
from SqlLog as s1 inner join SqlLog as s2
On s1.Module=s2.Module and s1.Login=s2.Login
Where (s1.Message like 'OLEDB - StartTransaction') and
  (s2.Message like 'OLEDB - Abort' or s2.Message like 'OLEDB - Commit')and
  (s2.IdSqlLog>s1.IdSqlLog) and
  (s2.IdSqlLog<(
   Select top 1 IdSqlLog
   from SqlLog as i
   Where i.Login=s1.Login and
     i.Module=s1.Module and
     i.IdSqlLog>s1.IdSqlLog and
     i.Message like 'OLEDB - StartTransaction'
   Order by i.IdSqlLog))


Traducido lo que hace es un join de la tabla de log con sigo misma juntando por Module y login, esto es porque es una aplicación multicapa, en una aplicación Cliente/Servidor una campo hostname en la tabla de log soluciona el problema del join.
Hecho el join busca un begin transaction y para ese busca o un commit o un rollback cuidando de que el idLog sea mayor y que no exista ningún otro begin transaction en el medio (Begin transaction del mismo Login/Module o hostname según corresponda).
Mañana estaré probando esto en un entorno de producción, con datos reales
WARNING
No hace falta pedir el plan de consulta para ver que esta consulta es explosiva especialmente en una tabla que crece sin descanso y que se puede volver gigante en un par de días










Así que mucho cuidado, ejecutarla sobre un set de datos pequeño o en un servidor de testing.

jueves, 16 de junio de 2011

DBExpress & Trace


Bueno, en esta oportunidad quiero comentarles una técnica que me ha dado excelentes resultados con muy poco trabajo. Se trata de dedicar algunos minutos, máximo alguna hora a la creación de un esquema de logueo de comandos de base de datos ejecutados por dbexpress que puede ser de suma utilidad en desarrollo e incluso en producción.

El Problema

Muchas veces en el día a día del desarrollo nos encontramos con alguna error de base de datos, o una operación que no se realizo como esperábamos, en definitiva una situación desconocida y no tenemos muchas herramientas para hacer un diagnostico rápido. De nada valen breakpoints porque no es un error de programa sino propiamente SQL.

La solución

La solución que quiero presentar es muy simple de implementar gracias el componente TsqlMonitor de DBExpress, no cuesta mucho trabajo y siempre es útil tener a mano algo mas de información para un rápido diagnostico. El Componente TsqlMonitor nos dispara una evento con cada interacción entre nuestra conexión y la base de datos, este evento viene acompañado de una buena cantidad de información sobre el comando que se acaba de ejecutar, existen una forma rapida mediante el componente de guardar esta información en archivo pero yo quiero ir un poco mas allá.

La implementación

Lo que propongo es guardar también esta información en la base de datos, esto nos permite rastrear y encontrar mejor lo que buscamos, permite una persistencia mas seria de la historia. Un diseño de tabla mas o menos completo seria el siguiente:

CREATE TABLE [dbo].[SqlLog](
[IdSqlLog] [int] IDENTITY(1,1) NOT FOR REPLICATION NOT NULL,--Clave principal autoincremental
[Module] [varchar](50) NULL,--Modulo que ejecuto el comando, muy util en DataSnap
[FechaHora] [datetime] NOT NULL,--FechaHora de ejecucion
[Message] [text] NOT NULL,--Mensaje, DDL DML correspondiente
[TraceFlag] [int] NOT NULL,--Informacion adicion DBExpress
[TraceLevel] [int] NOT NULL, --Informacion adicion DBExpress
[CustomCategory] [varchar](256) NOT NULL, --Informacion adicion DBExpress
[Login] [varchar](50) NULL, --Usuario logueado
[InTransaction] [varchar](50) NOT NULL, --¿El comando se ejecuto dentro de una transaccion?
CONSTRAINT [PK_idSqlLog] PRIMARY KEY CLUSTERED
(
[IdSqlLog] ASC
) )Etc....

Esto esta extraido de un sqlserver 2008 pero es mas o menos lo mismo en cualquier motor. Comento en cada linea cual es la idea del campo.
Campo mas campo menos la información esta, abajo un ejemplo real de mis aplicaciones


TSqlMonitor
Lo primero que hay que hacer luego de crear la tabla es drag and drop de un TsqlMonitor a nuestro DataModule o donde tengamos acceso a nuestro componente de conexión, mediante la propiedad SqlConnection enlazamos el Monitor a la conexión.


Si tenemos una datamodule o servermodule abstracto del cual heredamos mucho mejor, mas información sobre esto aquí


Ahora hay que tener mucho cuidado, no podemos usar la misma conexión para loguear lo que sucede, seria como verse entre dos espejos, la situación se extendería hasta el infinito en el caso de dbexpress hasta el access violation :)
La razón es simple, si usamos la misma conexión al tener que llevar una operación al log estaríamos generando otro trace (El insert al log) que también generaría otro comando para un nuevo trace de este comando y así hasta el desastre.
De esto se desprende que necesitamos otra conexión para cumplir con el objetivo, podemos usar directamente le componente de conexión o ayudarnos con un TsqlQuery con la siguiente inserción:

INSERT INTO SqlLog
(Module, Message, TraceFlag, TraceLevel, CustomCategory, Login, InTransaction)
VALUES (:Module,:Message,:TraceFlag,:TraceLevel,:CustomCategory,:Login,:InTransaction)

(Las fechas las pone la base), y un metodo que hago uso del comando mas o menos así:


function TDMGlobalConnection.eLogTrace(AModule:String; Msg:String; TraceFlag:Integer; TraceLevel:Integer; CustomCategory:String; UserName:String; InTransaction:boolean):boolean;
begin
Result := false;

//OLEDB - Commit
//OLEDB - StartTransaction


//256=Consulta
//512=Parametros

if ((TraceFlag=256) or (TraceFlag=512) and ((TraceFlag=32))) and
(
(Msg<>'OLEDB - Release')and
(Msg<>'OLEDB - ReleaseAccessor')and
(Msg<>'OLEDB - GetNextRows')and
(Msg<>'OLEDB - ReleaseRows')and
(Msg<>'OLEDB - GetData')and
(Msg<>'OLEDB - Execute')and
(Msg<>'OLEDB - GetResult')and
(Msg<>'OLEDB - CreateAccessor')and
(Msg<>'OLEDB - IAccessor')and
(Msg<>'OLEDB - GetColumnsInfo')and
(Msg<>'OLEDB - GetColumnInfo')and
(Msg<>'OLEDB - IColumnsInfo')and
(Msg<>'OLEDB - ICommandText')and
(Msg<>'OLEDB - SetCommandText')and
(Msg<>'OLEDB - GetResult')and
(Msg<>'OLEDB - ReleaseAccessor')and
(Pos('SELECT DISTINCT DB_NAME(), SCHEMA_NAME(O.uid)', Msg)=0)and
(AModule<>'TDSInterface')
) then begin

InsertLog.ParamByName('Module').Value := AModule;
InsertLog.ParamByName('Message').Value := Msg;
InsertLog.ParamByName('TraceFlag').Value := TraceFlag;
InsertLog.ParamByName('TraceLevel').Value := TraceLevel;
InsertLog.ParamByName('CustomCategory').Value := CustomCategory;
InsertLog.ParamByName('Login').Value := UserName;
InsertLog.ParamByName('InTransaction').Value := BoolToStr(InTransaction, true);
InsertLog.ExecSQL();

Result := true;
end;

end;

Se puede observar que no se esta logueando todo, se esta haciendo un filtro y esto es muy dependiente del motor, el lector puede loguear todo y luego ir quitando mediante filtros elementos que no lo interesan o no aportan a la causa.

¿Y con aplicaciones multithread?¿Y con DataSnap?

Bueno es básicamente lo mismo pero añadimos un complejidad mas, podemos tener las dos conexiones en cada servermodule, una para su uso regular y otra para log.
Sin embargo la cantidad de conexiones puede ser demasiada si operamos asi, la solucion tener un DataModule global, a este dataModule lo van a usar todos los server modules que necesite loguear. ¡Pero cuidado con la concurrencia! Debemos utilizar seccion criticas para asegurarnos que lo hagan de a uno a la vez, véase


Finalmente el evento quedaria así

procedure TAbstractDSServerModule.SQLMonitorTrace(Sender: TObject;
TraceInfo: TDBXTraceInfo; var LogTrace: Boolean);
begin

//Puede poner flag para desactivar log en producción o cuando no se desee
CSConnection.Enter;
try
DMGlobalConnection.eLogTrace(Self.ClassName, TraceInfo.Message, TraceInfo.TraceFlag, TraceInfo.TraceLevel, TraceInfo.CustomCategory, 'Proxima entrada', TSqlMonitor(Sender).SQLConnection.InTransaction);
finally
CSConnection.Leave;
end;


end;

end;


Prometo subir el código de todas las entradas en breve.

martes, 14 de junio de 2011

Threads y Secciones Criticas

Cuando se necesita asegurar el acceso atómico a un bloque de código determinado tenemos que utilizar alguna forma de bloqueo y sincronizacion. Delphi dispone de varias herramientas para esto (Semáforos, monitores, secciones criticas)
Vamos a ver una de ellas, probablemente la mas sencilla y en mi caso la única que he usado intensivamente.

Una aplicación VCL tradicional no usa mas que un thread o hilo, el hilo principal, por lo tanto no tenemos acceso simultaneo a un recurso. Esto hace que el tema de sincronizacion no nos toque directamente, cuando uno desarrolla una aplicación/servidor de aplicación o servicio multihilo la cosa cambia y hay que tener especial atención al acceso simultaneo a los recursos.

La sección critica
Como dijimos en delphi tenemos una herramienta muy sencilla que es la seccion critica, esta no es mas que una clase de la cual necesitamos solo 2 metodos, uno para entrar a la seccion critica o otro para abandonarla

CS.Enter;
try
 //mi código protegido de acceso simultaneo
finally
 CS.Leave;
end;

Observe que el código protegido esta dentro de un bloque try-finally, esto no es solo prolijidad o una buena practica, en este caso particular es mandatario. Si una excepción ocurre en “mi código protegido” y no se ejecuta el método Leave nunca se abandona la sección critica, por tanto se bloquean todos los otros threads que intenten ejecutar el código protegido.

Ejemplo para el uso de TcriticalSection en DataSnap

Dependiendo del modelo de lifeCycle seleccionado en cada uno de los ServerClass es muy probable que tengamos varios Threads funcionando en paralelo. ¿Y si quiero que mis ServerModules escriban operaciones, logs de seguimiento o lo que sea en un visor en la pantalla principal? 
Acá aparece el problema, supogamos ponemos un memo para loguear actividades de los ServerModules, varios intentaran usar el memo al mismo tiempo produciendo los problemas de concurrencia mencionados. 
Ponemos el ejemplo de escribir sobre un memo en la pantalla principal porque es el ejemplo mas sencillo, pero aplica a cualquier uso de un recurso compartido, en este caso el memo.


TCriticalSection

Antes de usar una sección critica hay que crearla, una unidad global puede ser una solución. Probablemente no es la mas elegante ni ortodoxa pero es la que aprendí y no he probado otra por tanto no me voy a arriesgar a proponer nada nuevo para mi.

unit uGlobal;
interface
uses SyncObjs;
var
  CSLog:TCriticalSection;
implementation
initialization
  CSLog := TCriticalSection.Create;
finalization
  CSLog.Free;
end.

Creada la sección critica esta lista para ser usada justo antes de acceder al recurso compartido...

procedure TAbstractServerModule.LogVisor(s: string);
begin
  CSLog.Enter;
  try
    FrmMain.LogVisor(s);
  finally
    CSLog.Leave;
  end;
end;

Ya que estamos, aprovecho mi ServerModule abstracto (ver http://pablosoligo.blogspot.com/2011/06/datasnap-dbexpress-y-threads.html)
para crear la función.Justo antes de usar el recurso compartido entro en la sección critica, justo luego de salir salgo de la misma.
Ahora todos los server modules puede escribir sobre el log, no importa que estén en distintos threads y que lo quieran hacer simultáneamente.

Recordar
  1. Try-Finally. No podemos dejar una sección critica sin salida, entonces si entro y siempre salgo
  2. Hacer la sección critica lo mas corta posible para favorecer paralelismo y multitarea 
En la medida que se avance en mas aspectos de desarrollo datasnap se daran mas usos a las secciones criticas. En breve el código disponible aquí









domingo, 12 de junio de 2011

DataSnap, DBExpress y Threads

El primer problema al que se enfrenta el desarrollador DataSnap principiante es el problema de las conexiones a la base de datos. La costumbre le hace rápidamente cometer un error. En su afán por ahorrar conexiones es sumamente probable que cree un datamodule y redireccione todas sus datasets a la conexión disponible en el mismo. Acá es donde aparece el problema, dependiendo del lifecycle seleccionado en el componente TDSServerClass es altamente probable que varios threads intenten usar esta conexión, dbexpress no es thread safe y para peor podemos generar una buena cantidad de problemas si todos los thread manejan las mismas conexiones, active recordset etc.

Solución:

Una conexión por ServerModule.
Esto abre un interrogante, ¿Tengo que tomarme el trabajo de meter una conexión y configurara directamente en cada uno de mis servermodules?¿Tengo que generar el código para tomar los parámetros del conexión en cada uno de mis servermodules?
La respuesta es no, podemos usar la herencia que tan bien funciona en Delphi para codificar una vez y reutilizar cuando se necesite.De hecho esta saludable practica no solo va a aportar soluciones a este problema, nos va a ayudar en otras problemáticas que aparecerán en escenarios futuros cuando profundicemos mas en esta tecnología. En futuras entradas podremos sacar mas provecho de lo expuesto aquí.
Vamos paso paso como hacer esto.

Crear el servidor datasnap
Si lo desea puede utilizar el wizard de delphi














Seleccione la opción VCL Forms Application, Habilite TCP/IP y HTTP (Por las dudas si lo necesita en el futuro, para esto no es necesario), y en la ultima acción del wizard seleccione TDSServerModule.
El servermodule creado automáticamente por el wizard solo servirá a los fines de heredar de el los servermodules que verdaderamente ofrecerán la funcionalidad, coloque un nombre que demuestre su condicion por ejemplo
uAbstractServerModule para el archivo.
TAbstractServerModule para la clase.

Verifique que dicho servermodule no figure como auto creado (Project->Options->Forms)

Vale aclarar que este server module "abstracto" en realidad no lo es tal, el nombre indica su condicion donde del software pero no es una clase abstracta.

Conexión en el ServerModule Abstracto
Lo llamamos abstracto por nunca se va a crear una instancia directamente sino que sera por medio de servermodules hijos. A pesar de esto tenemos cosas que hacer en modulo, comenzamos configurando una conexión en el dataexplorer y haciendo drag and drop












































Hagamos uso del evento beforeconnect de la conexión para levantar los parámetros de la misma en tiempo de ejecución















Usemos también los eventos de creación y destrucción del servermodule para abrir y cerrar conexión respectivamente















Ahora si, casi estamos, nos quedan algunos detalles pero vamos a crear nuestro primer servermodule "real", para ello seleccionamos File->New->Other->Inheritable items

















Seleccionado claro, como base nuestro servermodule abstracto.
Para ir terminando, creamos una archivo ini en la misma carpeta donde esta nuestros servidor DataSnap, copiamos los parametros de conexión de dbxConnections.ini como indica la figura.














Si se trabaja con SqlServer no olvidar el parametro MARS_Connection para habilitar "multiple active record set"















Ahora creamos el cliente, verificamos que los métodos en la clase abstracta están presentes en la clase proxy generada, el sistema de herencia funciona.


















Como dijimos en próximas entradas veremos mas ventajas de operar con herencia en servermodules

sábado, 11 de junio de 2011

DataSnap, algunos pensamientos

Quiero compartir algunos pensamientos sobre DataSnap, quiero hacerlo a modo de introducción sobre algunos artículos que quiero publicar sobre tópicos avanzados. Existe numerosa información sobre los primeros pasos, si aun no sabes lo que es DataSnap puedes empezar por aquí :

  1. Excelente documento donde Bob Swart nos da una buena revisión de DataSnap http://www.embarcadero.com/images/dm/technical-papers/delphi-2010-wp-datasnap-091016.pdf
  2. Si prefieres en Español pueden descargar el video de Jose Castillo sobre DataSnap http://edn.embarcadero.com/es/article/40336. (Aparentemente requiere login en EDN)


Creo que DataSnap, al menos en la actualidad, es una gran tecnología de desarrollo multicapa. Yo la estoy utilizando hace solo 2 o 3 años, anteriormente si bien había escuchado de la tecnología no la tenia muy en cuenta por varias razones
  1. Suponía un modelo de comunicación basado en COM/DCOM etc, elementos de MS que yo no domino totalmente y que las experiencias que había tenido con estas tecnologías fueron frustrantes. Esta suposición miá era parcialmente cierta
  2. Suponía a DataSnap directamente relacionado con desarrollo de software del tipo administrativo/comercial, ERP, CRM etc. Este tipo de desarrollo no son mi especialidad y mi orientación al software de tiempo real, o sistemas de bajo nivel me alejaban de DataSnap. Esta suposición hoy no tiene sentido y en el pasado algunas mediciones podrían haber demostrado si estaba en lo cierto o no pero me inclino a pensar que mi suposición fue prejuiciosa

En cualquier caso es una opción saludable en muchos escenarios, uno de sus puntos mas fuertes es la productividad, la interfaz IappServer puede hacer mucho por nosotros.
En desarrollos del tipo administrativo/comercial la sobrecarga de trabajo usando DataSnap es realmente muy baja ganando todas las ventajas del desarrollo multicapa. En el caso de aplicaciones/servicios de tiempo real se pueden evitar muchos dolores de cabeza dejando que DataSnap se ocupe de los detalles TCP/IP, transferencia, paquetes, protocolo, cifrado, logueo, compresión etc.

¿Porque mejor no usar webservices?
Bueno, personalmente soy un fiel adepto a los webservices, tanto en Delphi como en .NET según el caso. Creo que en muchos escenarios la opción webservices es la mas recomendable, pero el desarrollo sobre DataSnap tiene algunos ventajas sobre los webservices que quiero recordar.
  • Interfaz IappServer, si estas pensando en desarrollo intensivo sobre bases de datos con DataSnap la productividad crece exponencialmente respecto a los webservices
  • Otro aspecto importante en términos de productividad, el modelo drag and drop de componentes sigue firme en la creación de servidores DataSnap, simplificando y acelerando el desarrollo.
  • Velocidad, la transferencia “Stream” de datasnap lo hace mucho mas eficiente, y , hay que decirlo, mas oscuro. Una de las cosas que mas me gusta de los webservices SOAP es su transferencia XML. Claro, también se puede implementar REST o SOAP sobre datasnap, al menos sobre REST y hasta donde llego perdemos las interfaz IappServer. (Todo no se puede :))

Cuando no usarlo
Primero lo mas obvio, no hay que usar DataSnap cuando es una decisión tecnológicamente irracional, con esto quiero decir, que no se les ocurra transferir audio y video en tiempo real con DataSnap porque los resultados no van a estar a la altura de lo esperado. Resulta lógico, y acá la aclaración, DataSnap trabaja sobre TCP/IP en su modo de mas bajo nivel, adicionalmente se puede montar una capa http. Mas allá que sea parte de los manuales de protocolos de red mi experiencia es que no hay que llegar al extremo de video on-line para ver que TCP/IP para algunas cosas puede ser muy lento. Cuando se necesita comunicación fluida en tiempo real entre n aplicaciones no queda otra que el sencillo UDP, como dijimos audio, video, movimiento o interacción de elementos en tiempo real. Claro, en una aplicación de tipo administrativo los tiempos son otros y no hay tanto apuro, incluso en aplicaciones de tiempo real tampoco hay tanto apuro para informar la mayoría de las cosas, como dijimos todo depende. Claro, sino aplica DataSnap en estos casos, mucho menos aplica la opción webservices.
No hay muchos mas argumento en contra, podríamos agregar que si el servidor DataSnap tiene que interactuar exclusivamente con aplicaciones desarrolladas con herramientas no embarcadero quizá webServices sea una mejor opción, aunque como dijimos, nada impide consumir métodos DataSnap via REST o por otro medio estandar.
En la siguiente entrada quiero meterme con algunos consejos para el desarrollo real en DataSnap, como mencionamos, existen muchos tutoriales y videos sobre los primeros pasos con esta tecnología pero poco se habla sobre algunos temas mas avanzados, quiero hacer foco en eso.