martes, 21 de junio de 2016

Las expresiones regulares no son mi fuerte

Necesitaba hacer una expresión regular que me permitiera cambiar de las sintaxis

Call(Objeto,&ParametroIN, ......, &ParametroOUT)

a

&ParametroOUT = Objeto.udp(&ParametroIN,......)

La expresión regular mas sencilla que encontré (con la invalorable ayuda de Nicolás) , que funciona bien en los casos que los probé es:

(\s+)?(call\(({0})([\s\b]*)(,(.*)(,[\s\b]*)(&([a-z0-9\-]+))|,(.*)(&([a-z0-9\-]+)))[\s\b]*\)|(({0})(.call) ?\(((.*)(,[\s\b]*)(&([a-z0-9\-]+))|(.*)(&([a-z0-9\-] +)))[\s\b]*\)))

donde debo remplazar {0} por el nombre del objeto.

Luego debe remplazar en el código con la expresión super amigable:

$1$8$11$19$22 = $3$14.udp($6$17)

La potencia de las expresiones regulares es asombrosa, pero también es asombroso lo complejas que pueden quedar con casos bastantes sencillos.

PD: La próxima version de KBDoctor, tendrá una opción que identifica los programas que tienen todos los parámetros IN, menos el ultimo OUT y cambia todas las invocaciones al formato UDP. Esto hace el código mas claro y que GeneXus detecte algunos errores al especificar.

UPDATE: Tuve que actualizarla a
       
(\s+)?(call\([\s\b]*({0})([\s\b]*)(,(.*)(,[\s\b]*)(&([a-z0-9\-]+))|,(.*)(&([a-z0-9\-]+)))[\s\b]*\)|(({0})(.call) ?\(((.*)(,[\s\b]*)(&([a-z0-9\-]+))|(.*)(&([a-z0-9\-] +)))[\s\b]*\)))

Metí la pata

Hace unas semanas, estaba con gripe en casa. Estaba aburrido y me puse a ver que podía mejorar una KB Genexus. Para esto, me puse a probar una opción de KBDoctor que permite identificar cuales son los objetos que tienen problemas y arreglar los mas comunes.

Los tipos de problemas que soluciona son:

  • variables que no están basadas en dominios / atributos
  • parámetros que no tienen indicador de in/out
  • atributos que no tienen dominios 
Estos arreglos son semiautomáticos, donde KBDoctor cambia el fuente en forma automática, pero  luego tiene que verificarse en forma manual, pues el cambio no siempre es el correcto, porque se usan heuristicas que no siempre hacen lo correcto. 

Primero probé estos  con una KB chica y bien conocida y todo funcionó bien. 
Con el viento en la camiseta y pensando que nada podía salir mal, hice una corrida en una KB grande que están en desarrollo desde el año 1998, que tiene código viejo. 
Tal vez por la gripe y la fiebre, cuando quise acordar había cambiado mas de 1600 objetos.  Ya había subido muchos objetos, cuando me di cuenta que el cambio podía tener efectos complicados. 

En ese momento, debería haber valorado el cambio y hacerlo mas paulatino. Si bien el cambio agrega mucho valor en la KB, el hacerlo en forma tan repentina, puede dificultar la detección de los problemas.  Mande un mail a todo el grupo de desarrollo tratando de explicar la situación y advirtiendo que podía haber metido algún bug con el cambio. 

El código llego a producción y efectivamente tuvimos tres errores que afectaron la operativa del sistema.  Los problemas fueron que parámetros que tenían que ser OUT, quedaron como IN: . GeneXus no daba error, porque los mismos eran modificados en llamadas a procedimientos.

Para peor, de los errores detectados, dos ya los habíamos detectado y corregido en el ambiente de desarrollo, pero no me di cuenta que esos errores debían pasarse en forma urgente al ambiente de stage para pasar a producción lo antes posible.

En fin, son varios errores de los cuales se pueden sacar muchas experiencias (para no repetir errores)

  • Si se hacen cambios grandes, hay que testear mucho mas. 
Tenemos definido un proceso de deploy diferente (escalonado) para cuando hacemos migraciones (que son cambios grandes) y en este caso no lo usamos. 
  • No alcanza con corregir errores, hay que asegurarse que los mismos lleguen a los ambientes adecuados en los momentos adecuados. 
  • Los cambios de código en forma automática, necesitan mas control que los cambios manuales. 

Esto nos costó que todo el grupo de desarrollo tuvo que revisar los objetos que habían cambiado las reglas de parámetros (eran muchos), el costo de la imagen del sistema, que por un buen rato (mas de 4 horas) estuvo funcionando mal y muy probablemente una multa por parte del cliente.

A pesar de todo, estoy convencido que hay que rejuvenecer el código viejo, para hacerlo mas mantenible y que no nos de sorpresas en el futuro. Solo que la próxima vez, tendré la precaución de hacerlo con mas cuidado. 


También tengo que recordar, que el año que viene no debo vacunarme contra la gripe, de forma de no tener que quedarme toda un semana en casa con tos y dolor de garganta.

 

domingo, 12 de junio de 2016

Limpiando una KB GeneXus - un ejemplo práctico.

Una empresa amiga me mando una KB, para que diagnosticara algunos problemas de performance bastante serios que estaban teniendo, en el proceso de build de la misma.

Es una KB recién migrada desde GeneXus 9.0 a GeneXus Evolution 3 U8. Me mandaron el MDF (archivo de la base de datos SQL Server) de la KB ya convertida por ellos y con eso me puse a trabajar.

Esta KB me dio la oportunidad de practicar una limpieza a fondo de la misma, en código escrito por otros, que me permite probar el KBDoctor en un entorno diferente al que tenemos en Concepto.

Al empezar la KB tenia:

13.872 objetos. 10.662 atributos y 674 tablas.

Con esos datos iniciales, me puse a trabajar.

Revisar propiedades.
Lo primero que hago cuando trabajo con KB grandes, es revisar algunas propiedades de las mismas.
En este caso lo que hice fue:

  • Deshabilitar el Indexado dentro de la KB (esto enlentence mucho cada vez que se salva un objeto)
  • Poner el Generate Developer Menu en *NO
  • No generar prompts. 


Build all. 

Hice un build all con la KB sin haberle cambiado nada y daba el SQL Server daba timeout, al intentar hacer el impacto de la base de datos.
Habilite el profiler en mi SQL Server y vi que la causante era una sentencia sql que intenta copiar todos los registros de un modelo (diseño) al de produccion y la problematica era la tabla ModelCrossReference que tiene aproximadamente 19.000.000 registros (tengo 3 modelos con 6.392.000 cada uno aprox)  y estaba bastante fragmentada.

Defragmentar las tablas SQLServer. 

Para defragmentar tablas yo utilizo los scripts de https://ola.hallengren.com/sql-server-index-and-statistics-maintenance.html, que son faciles de usar y dan muy buen resultado. Luego de usarlo, no volvio a aparecer el problema del timeout al hacer el copymodel.

Borrar los atributos que no estan en ninguna tabla. 

Es comun que las KB que vienen de 9.0 tengan atributos definidos que se han dejado de usar, y ya no estan en ninguna tabla. Hay una opcion en KBDoctor para borrarlos y cambia (casi) todas las referencias de objetos a dichos atributos y luego los borra.
Despues de esta primer limpieza quedo asi

13.872 objetos, 10.597 atributos y 674 tablas. 

Borrar objetos no referenciados. 

Use la opción de KBDoctor que borra todos los objetos que no son referenciados por nadie.
La KB no tenia llamadas dinámicas entre objetos, por lo que es seguro borrar todos los objetos que nadie referencia. La opción del KBDoctor que borra los objetos no referenciados, también inicializa la transacciones que nadie llama, borrando las variables, poniendo el formulario por defecto en WEB y WIN y borra todos los eventos.
Luego de este paso quedo con los siguientes números

13.121 objetos, 10.597 atributos y 674 tablas.

Limpieza de variables 
Viendo que alguno de los problemas de performance venían por el lado de las referencias cruzadas entre objetos, me puse a pesar de que forma podía achicarse dichas referencias. Una forma de sacar referencias es borrar las variables no usadas, que hagan referencias a atributos. Para esto, nada mejor que usar la extensión Variables Cleaner de Dvelop.

Demoró muchísimo (mas de 9 horas) y terminó cambiando 9223 objetos y borró mas de 237.000 variables.

Total cleaned objects: 9223. Removed variables: 237885
Variables Cleaning Success

Nunca habia visto una KB que tuviera tantas variables definidas pero no usadas.

Al empezar esta limpieza, la tabla ModelCrossReference tenia mas de 6.300.000 registros para el ModelId = 1 (el modelo de diseño) y al terminar tenia 1.170.746. Eliminó mas de 5 millones de registros!!.
No conozco como se guardan las referencias, pero me parecen demasiados registros. Tal vez en la conversion de versiones viejas pueda quedar algo mal.

Fue la primera vez que me puse a pensar que tener muchas variables definidas en los objetos podia enlentecer el proceso de build all.


Borrar objetos que no se usan mas. 

Era común en la época anterior a GeneXus Server, tener carpetas BORRAR o similares, que tenian todos los objetos que debían borrarse o no se usaban mas, pero se dejaban para no perder la historia de los mismos.
En esta KB encontré un folder BORRAR con muchos objetos y desmarque de ella todos los objetos main y volví a correr la opción que borra los objetos no referenciados.

Despues de la limpieza y el build all quedo

modelid     registros
----------- -----------
1           1.150.140
3           1.150.140
4           6.391.870

El modelId=4 es una version que creé antes de empezar la limpieza.
El modelId=1 es diseño y el modelId=3 es el modelo en el que estoy trabajando y tiene la misma cantidad de registros que el de diseño pues acabo de hacer una reorganización.

12.938 objetos, 10.597 atributos y 674 tablas.

KBCompress. 

Luego de toda la limpieza, conviene chequear si todo esata bien.
Para esto utilice el KBCompress de las KBTools, que abre la KB con la opcion de CompressData=true y ademas hace un CheckKnowledgeBase, que revisa si esta todo bien desde el punto de vista interno de la KB.
Demoró 18 minutos y si bien acomodó varios indices no encontro ningun error a corregir.

KBCompressMDF

Con todas estas operaciones, la base de datos de la KB, había crecido hasta los 3.3Gb y el archivo LDF tenia mas de 4GB.
Decidí entonces ejecutar el KBCompressMDF de las KBTools, que permite achicar los archivos de la base de datos de la KB.
Despues de ejetuar esto, los archivos de la KB ocupaban 1.8Gb los datos y 500Kb el LDF.
De mas de 7Gb a menos de 2Gb, por lo que en la limpieza recuperamos unos 5Gb.

Hay que recordar que cada vez que se ejecuta una compresión de los archivos de la base de datos, se mueven muchas paginas de datos, lo cual puede llevar a tener fragmentación de las tablas. Conviene por eso, volver a correr la Defragmentacion de indices de las tablas de SQL server del paso inicial.

Resumen 

Pasamos de tener

13.872 objetos. 10.662 atributos, 674 tablas, 3.8Gb, 6.391.870 registros en ModelCrossReference
12.938 objetos, 10.597 atributos, 674 tablas, 2.2Gb, 1.150.140 registros en ModelCrossReference

Diferencia
934 objetos,              65 atributos,      0 tabla,    1.6Gb, 5.241.730 registros menos. 


Ademas, quedaron marcadas varias transacciones con la propiedad Generate Object=*NO lo cual achica aun mas los tiempos del Build All.

Todo esto, generando exactamente los mismos ejecutables, solo eliminando lo mas grueso.

Faltaría aun hacer otra limpieza mas profunda, donde borremos tablas no usadas, transacciones que no se necesitan, objetos no alcanzables (que se llaman entre si y por eso no se borraron la primer limpieza), atributos que están en las tablas pero no se usan, etc, pero todo eso puede quedar para otro post.

UPDATE:
Me preguntar cual era el error que daba al hacer el build all. Lo pongo aca abajo.

========== Build All started ==========
========== Target Environment update started ==========
error: System.Data.SqlClient.SqlException: Valor de tiempo de espera caducado. El período de tiempo de espera caducó antes de completar la operación o el servidor no responde.
   en System.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection)
   en System.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj)
   en System.Data.SqlClient.TdsParserStateObject.ReadSniError(TdsParserStateObject stateObj, UInt32 error)
   en System.Data.SqlClient.TdsParserStateObject.ReadSni(DbAsyncResult asyncResult, TdsParserStateObject stateObj)
   en System.Data.SqlClient.TdsParserStateObject.ReadNetworkPacket()
   en System.Data.SqlClient.TdsParserStateObject.ReadBuffer()
   en System.Data.SqlClient.TdsParserStateObject.ReadByte()
   en System.Data.SqlClient.TdsParser.Run(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj)
   en System.Data.SqlClient.SqlCommand.FinishExecuteReader(SqlDataReader ds, RunBehavior runBehavior, String resetOptionsString)
   en System.Data.SqlClient.SqlCommand.RunExecuteReaderTds(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, Boolean async)
   en System.Data.SqlClient.SqlCommand.RunExecuteReader(CommandBehavior cmdBehavior, RunBehavior runBehavior, Boolean returnStream, String method, DbAsyncResult result)
   en System.Data.SqlClient.SqlCommand.InternalExecuteNonQuery(DbAsyncResult result, String methodName, Boolean sendToPipe)
   en System.Data.SqlClient.SqlCommand.ExecuteNonQuery()
   en Deklarit.Data.ReadWriteCommand.ExecuteStmt()
   en gxioSQL.ModelCrossReferenceDataReader.DeleteByModelId(Int32 modelId)
   en Artech.Udm.Layers.Data.SQL.Mappers.ModelCrossReferenceMapper.DeleteByUdmModel(UdmModelKey udmModel)
   en Artech.Udm.Layers.BL.Managers.ModelCrossReferenceManager.DeleteByUdmModel(UdmModelKey udmModel)
   en Artech.Udm.Framework.UdmKnowledgeBase.BatchCopyModel(Model source, Model target)
   en Artech.Udm.Framework.UdmKnowledgeBase.CopyModel(Model source, Model target, CopyModelOptions options, IEnumerable`1 objectTypes, IEnumerable`1 copyOutputIds)
   en Artech.Architecture.Common.Objects.KnowledgeBase.CopyModel(KBModel fromModel, KBModel toModel)
   en Artech.Core.UI.Services.KBService.CopyModel(KBModel fromModel, KBModel toModel)
   en Artech.Packages.Genexus.UI.Commands.CopyModelCommand.Do()
Target Environment update Failed
Build All Failed

Y la sentencia que daba problemas era

with tgt as (select * from ModelCrossReference where ModelId = @ModelIdTarget) merge tgt
using (select * from ModelCrossReference where ModelId = @ModelIdSource) src
on tgt.ToEntityTypeId = src.ToEntityTypeId and tgt.ToEntityId = src.ToEntityId and tgt.FromEntityTypeId = src.FromEntityTypeId and tgt.FromEntityId = src.FromEntityId and tgt.LinkType = src.LinkType and tgt.LinkTypeDetailId = src.LinkTypeDetailId
when not matched by target
then insert values(@ModelIdTarget, ToEntityTypeId, ToEntityId, FromEntityTypeId, FromEntityId, LinkType, LinkTypeDetailId, LinkTypeInfo, ReferenceType)
when matched and (tgt.LinkTypeInfo != src.LinkTypeInfo or tgt.ReferenceType != src.ReferenceType)
then update set tgt.LinkTypeInfo = src.LinkTypeInfo, tgt.ReferenceType = src.ReferenceType
when not matched by source then delete







jueves, 2 de junio de 2016

Programas que mejoran el código GeneXus.

En el post Codigo GeneXus mas entendible, contaba algunas técnicas muy sencillas para hacer que los programas GeneXus sean mas fáciles de entender y por lo tanto de modificar y mantener.

También había comentado en otro post que cada vez mas usamos mas programas para generar programas o para hacer tareas propias del desarrollo de sistemas.

Para seguir con esta tendencia, hice algunos programas para cambiar programas para que los mismos sean mas entendibles.

Los cambios que por ahora esta haciendo son:


  • cambio de palabras clave (for each, where, do, case, endif, if, etc) para pasarlas a minúsculas
  • pasar Call(Objeto) a sintaxis objeto.call() o objeto()
  • detectar variables no basadas en atributos y dominios


Las dos primeras se pueden hacer sin pedir ayuda al desarrollador, pero la tercera, no es fácil de automatizar.
Lo que si se puede hacer, es aprender de las decisiones del usuario, o sea, si en un objeto tengo una variable que se llama FilePath y es Char(100) puedo preguntarle al usuario y asignarle el dominio Path.
El programa puede "aprender" o "recordar" ese cambio y hacerlo para el resto de los programas de la KB. Esto es, cada vez que encuentre una variable FilePath Char(100) puede asignar ese dominio en forma automática.

También ayuda  poniendo IN y OUT en las reglas parm(), pero esto no puede ser 100% automático, porque es bastante peligroso, por lo que dejo que el usuario haga un revisión de los cambios.

Con un esfuerzo razonable, se pueden mejorar bastante los programas para que sean un poco mas fáciles de entender.

miércoles, 1 de junio de 2016

Código GeneXus mas entendible

Estuvimos haciendo charlas internas sobre como hacer mas fácil de entender el código GeneXus que escribimos.

Algunas cosas básicas que vimos:

  • Nomenclatura de objetos y variables
  • Descripción, Column Title, Contextual title, etc en atributos. 
  • Descripción de Tablas y Grupos de Subtipos (esto lo ven los usuarios)
  • call(Objeto) vs Objeto.call() vs Objeto()
  • Parámetros con IN y OUT
  • Todos los atributos con Dominios (con significado semántico igual)
  • Todas las variables basadas en  atributos o dominios
  • Dominios Enumerados (para evitar constantes en el código)
  • Bloques de código cortos (no mas de 200 lineas)
  • Anidación de sentencias (no mas de 5 niveles de IF/Do/For Each, etc)
  • Comentarios innecesarios (no dejar código viejo comentado). 
  • Palabras claves en MAYUSCULA o minúsculas
  • Indentación de for each / where
  • Orden de los Eventos
Si bien para algunos temas no hay demasiada discusión para algunos si hay varias posiciones diferenes. 

Por ejemplo, en el caso de las Mayúsculas o minúsculas en las palabras claves de GeneXus teníamos:


FOR EACH
ENDFOR

For each
Endfor

For Each
EndFor


for each
endfor

Por votación salio la opcion 3 (a mi me gusta mas la 4)

IF (Condición)
ENDIF

If (Condición)
EndIf

if (Condición)
endif

En lo personal, siempre prefiero todo en minusculas, pero creo que lo que mas me dificulta entender el codigo, es cuando se tiene diferentes formas de escribir la misma palabra clave, por ejemplo si se tiene 

If <Condicion1>
   if <Condicion2>
       IF <Condicion3>
       ELSE
           //sentencias1
       ENDIF
   else
// sentencias2
   endif
Else
//sentencias3
EndIf

me cuesta mas entenderlo que si se tiene todo en minúsculas. Mi cerebro tiene que pensar mas de una vez para entender cada una de las formas diferentes que esta escrita cada una de las palabras claves.

Con pequeñas cosas bastantes simples y conocidas, el código queda mas fácil de entender y al entenderlo mas rapido, podemos ser mas productivos y tener menos errores.

Estaria bueno que el codigo GX se guardara de forma standard y que cada desarrollador pudiera verlo como mas le gusta, adaptandolo en el momento de editarlo.