Cálculo de huecos alternativo para Eneboo

Para que varios usuarios puedan crear documentos de facturación a la vez y no bloquearse ni generar números duplicados Eneboo tiene una estrategia bastante común, que es, asignar la numeración al principio fuera de transacción y comprobarla al final de nuevo.

El problema de esto es que, aun con sólo un usuario, esto lleva a generar huecos, pues cada vez que un documento se intenta crear y se cancela, la secuencia numérica se incrementa y nunca vuelve al lugar original.

Para evitar estos huecos, en las facturas, se lleva un control de huecos de modo que cada vez que se borra una factura existe un recálculo que contempla estos huecos y los reaprovecha. Incluso al generar nuevas facturas, los huecos suelen ser calculados al vuelo.

Otras cosas que también se pueden hacer pasan por delegar el cálculo del número al final. Para ello, hay que marcar el documento como “Borrador” y disparar el cálculo al hacer el guardado final si se encuentra la marca activa. Hay dos variantes de esto, una es establecer una serie de facturación para el borrador, y al grabar definitivamente, se mueve el documento a su serie real. El problema es que el usuario no ve el número hasta que no graba del todo y que podría, por error, llegar a imprimir el documento en otra serie. La otra opción es que se pueda llamar inicialmente la función de cálculo sin que incremente o reserve el número cuando está en modo borrador, de este modo el usuario ve el número propuesto y al grabar definitivamente éste puede cambiar.

Esto es prácticamente lo máximo que se puede hacer con un sistema que tiene que ser compatible con MySQL y PostgreSQL a la vez. Pero, ¿y si sólo se diese soporte a PostgreSQL?

Según que versión queramos dar soporte, hay diversas funcionalidades disponibles:

  • PostgreSQL 9.5 introduce “SKIP LOCKED” en las selects, permitiendo gestionar colas y reservas de trabajos. Este es el sistema ideal para solucionar el problema
  • PostgreSQL 9.1 introduce pg_try_advisory_xact_lock que se puede usar para lo mismo de un modo más manual.
  • PostgreSQL 8.2 introduce pg_try_advisory_lock, que es lo mismo que el anterior pero va por sesión, por lo que el programa debe recordar soltar el bloqueo cuando se cancela la operación.

En todos ellos el resultado es el mismo, una vez se pide el número, queda reservado hasta fin de transacción, por lo que crear y cancelar desde un solo puesto no genera huecos. Pero si un usuario pasa 20 minutos creando algo que luego cancela, y mientras tanto otro crea otro documento, ese documento tendrá un número más y existirá un hueco.

No voy a entrar en una implementación exacta, pero sí un esbozo de cómo funciona cada sistema.

Usando Skip Locked

PostgreSQL 9.5 introdujo una característica muy interesante en las selects llamada “SKIP LOCKED. Con ésta podemos realizar una select que omita los registros bloqueados. Y esto, ¿de qué nos sirve? Sencillo: Primero generamos una tabla con las secuencias futuras (pongamos por ejemplo los próximos 50 albaranes de la serie) con antelación. Luego, usamos una select que comina FOR UPDATE y SKIP LOCKED a la vez, en plan:

SELECT numero FROM secuencias_postgres WHERE tipo=’facturascli’ AND codejercicio=’2017′ and codserie=’A’ ORDER BY numero ASC FOR UPDATE SKIP LOCKED;

Esto, de forma efectiva, nos devuelve el primer número de la secuencia disponible, bloqueándolo para que otras transacciones no lo puedan coger. La siguiente transacción, gracias a SKIP LOCKED, omitirá el registro y cogerá el siguiente. Para hacerlo permanente, la siguiente operación debe ser un DELETE:

DELETE FROM secuencias_postgres WHERE id=3333;

Con esto garantizamos que si la transacción hace commit, el registro desaparece de forma efectiva. Pero si se cancela, el registro reaparece. Y mientras está en curso las demás transacciones no lo ven siempre y cuando usen SKIP LOCKED.

Creo que no necesita mucha más explicación, ¿verdad? Es bastante sencillo.

Cuando la transacción haga rollback, la siguiente verá su registro y lo bloqueará, llenando ese hueco inmediatamente.

Hay que acordarse de ir llenando la tabla o nos quedaremos sin secuencias. Para ello lo mejor es una tarea programada que se asegure de que al menos hay X. Y para facturación masiva hay que tener en cuenta que deben existir al menos X registros que vamos a necesitar, porque si pretendemos generar 1000 facturas pero solo hay 50 secuencias vamos a tener un problema.

Usando pg_try_advisory_xact_lock

Esta función permite generar bloqueos de aplicación basándose en un bigint o en dos ints de 32bits. Ya hubieran podido dar soporte a strings, pero nos las podemos apañar usando la función hash().

Lo primero es definir una transformación de nuestra secuencia tipodoc+codejercicio+serie+numero al tipo int+int. Una solución básica es usar: hash(tipodoc||codejercicio),hash(serie||numero)

Luego la estrategia es exactamente igual que la anterior, con la diferencia de que la función nos devuelve true o false, por lo que tenemos que probar hasta encontrar la primera que nos sirva. Entonces, empezamos por el MAX(numero)+1 que exista ahora mismo y probamos hasta que nos de true por primera vez:

numero = SELECT max(numero)+1 FROM (…)
numero_reservado = null
for (i=0;i<1000;i++) {
key = hash(…) + hash(…+(numero+i))
if SELECT pg_try_advisory_xact_lock(key)  {
numero_reservado = numero+i; break;
}
}
Este simple algoritmo (en pseudo código) nos dará el primero libre. Como es transaccional, al abortar se cancelará el bloqueo de todos modos y la siguiente transacción tendrá acceso a él.

Usando pg_try_advisory_lock

Esta función trabaja exactamente igual que la anterior, por lo que el código es exactamente el mismo. La única diferencia es que el bloqueo se libera al terminar la sesión. Por ello, deberemos capturar el cancelado o el rollback para manualmente liberar el bloqueo y así otras transacciones pueden hacer uso de él sin esperar a que se cierre Eneboo.

La función para esto es: pg_advisory_unlock(int1, int2)

La implementación del unlock es trivial, pues es calcular la misma key y desbloquearla. El mayor problema reside en capturar correctamente el cancelado o rollback, que es algo que depende de qué versión de Eneboo tengamos hay más o menos herramientas disponibles.

De todos modos postgresql 9.1 salió a finales de 2011 y ningún cliente debería tener versiones anteriores a ésta a fecha de hoy, ya que el soporte es de 5 años y el de la 9.0 terminó en septiembre de 2015.

Buscando huecos de forma eficiente con Postgres

Pongamos que hay para un año y serie, más de 10.000 albaranes y lanzamos un cálculo de huecos. Los cálculos normales toman bastante tiempo porque hay que comparar todas las opciones posibles y descargar los datos y tratarlos desde la aplicación es bastante lento.

Una solución es mover toda la consulta a PostgreSQL. Un ejemplo podría ser:

SELECT numero::int FROM albaranescli WHERE codejercicio=’2016′ AND codserie=’A’
EXCEPT
SELECT generate_series(1,(SELECT max(numero::int) FROM albaranescli WHERE codejercicio=’2016′ AND codserie=’A’ ))

Esto nos devuelve todos los números que faltan al contar desde 1 hasta el máximo que hay en ese momento. Para 30.000 albaranes, esta consulta tarda sobre 300ms, teniendo en cuenta que se consulta lo mismo dos veces (y se podría hacer sólo una con un CTE) y que la transformación a entero hace que los índices valgan para poco.

Otra solución pasa por buscar sólo entre las últimas semanas, ya que a veces tiene poco sentido rellenar un hueco de Enero cuando estamos a Julio (y debe hacerse con cautela)

Esto requeriría agregar más pantallas a Eneboo para permitir el cálculo completo o configurar el alcance del parcial.