Casos de uso para Apex asíncrono

Partiendo del artículo anterior donde vimos como Salesforce gestiona la ejecución de los procesos asíncronos  veamos como seleccionar el/los método/s  más adecuado/s en casos de uso reales.

Recordemos las posibilidades que tenemos:

  1. Utilización de la anotación @Future
  2. Implementación de la interfaz Schedulable
  3. Implementación de la interfaz Queueable
  4. Implementación de la interfaz Batchable

Empezaré por los más fáciles para evolucionar los casos de uso, a situaciones más complejas con las que me he encontrado.

Si al principio te aburres no dejes de leer, quizás lo encuentres más interesante a medida que avanzan los casos de uso, y please envíame tus comentarios para que podamos aportar nuevas opciones.

Si por el contrario, crees no tener mucho conocimiento, no es problema, la complejidad de los escenarios, va aumentado, pero siempre de forma escalada.

Espero ayudar así a todos los niveles de conocimiento.

1. Se requiere ejecutar un proceso asíncrono modificando levemente el código

Habitualmente este será el caso en el que ya tengamos un método en nuestra clase o vayamos a programar uno nuevo para realizar una operación concreta de forma asíncrona, para no bloquear la operativa del usuario. Ejemplos típicos son realizar una operación compleja sobre datos o invocar un Web Service cuya respuesta no podemos asegurar que sea inmediata.

En este caso la anotación @Future es la candidata ideal, para un método que debe ser estático. La transformación es casi inmediata y seguramente con pocos cambios, podremos tener unos Tests adecuados.

2. Se requiere que un método de una clase se transforme en asíncrono y que reciba como argumentos sObjects

Desafortunadamente una limitación de los Jobs anotados con @Future, es: parameters must be primitive data types, arrays of primitive data types, or collections of primitive data types. Methods with the future annotation cannot take sObjects or objects as arguments.

Por tanto deberemos optar por la creación de una clase que implemente la interfaz Queueable o Batchable.

3. Se requiere que un método de una clase se transforme en asíncrono y devuelva X, o que no sea estático

Análogamente al caso anterior, una limitación de los métodos Future es: must be static methods, and can only return a void type.

4. Se requiere que un Job se ejecute a una hora concreta.

En este caso la mejor opción es la creación de una clase que implemente la interfaz Schedulable.

Aunque poco comentado habitualmente, es posible la implementación conjunta de las interfaces Schedulable y Batchable en la misma clase, obteniendo la capacidad de ejecución de un proceso Batchable pero con ejecución programada (en un caso posterior adjunto captura de pantalla de un ejemplo).

5. Se requiere un Job que se ejecute de forma recurrente

Igual que el anterior, solo es necesaria adoptar la expresión CRON adecuada. Para la construcción de expresiones CRON existen múltiples webs.

6. Se requiere la ejecución de varios Jobs de forma secuencial

Existen 2 opciones:

La opción 1: Creación de una clase que implemente la interfaz Queueable y como última sentencia del método execute, lanzar el siguiente proceso mediante System.EnqueueJob(new ProcesoSiguiente()), lo que llamamos habitualmente Job Chaining.

La opción 2: Menos común y documentado, existe la posibilidad  de crear de una clase que implemente la interfaz Batchable y como última sentencia del método finish, lanzar el siguiente proceso mediante Database.executeBatch(new Proceso_Siguiente()), que es el mismo concepto de Chaining visto anteriormente, pero con las capacidades de Batchable.

6. Se requiere un único Job sobre una gran cantidad de registros pero en segmentos secuenciales

Este caso de uso, es una idea original de Scott Lee, explicado en Pluralsight.

Este caso simula una ejecución iterativa sobre una misma clase (laxamente hablando), sobre un conjunto de registros (podría ser muy elevado) utilizando un índice para determinar los paquetes que aún no han sido tratados.

Como creo que No se entiende nada, intento explicarme mediante un ejemplo:

  • Supongamos que tenemos 1 Millón de registros de Accounts. Estas Accounts tienen un Custom field, provincia.
  • El requerimiento consiste en procesar la información (para realizar un cálculo cualquiera) secuencialmente por provincias ordenado:
    • primero debemos considerar solo los Accounts de Álava
    • al haber finalizado ese proceso debemos abordar los de Albacete
    • al haber finalizado ese proceso los de Alicante
    • … y así hasta finalizar con todas las provincias hasta finalizar

Es decir solo cambian los datos, pero el Job siempre es el mismo, no queremos concurrencia, dado que podríamos estar accediendo a un recurso limitado (por ejemplo un Web Service, o una transacción que no admite concurrencia, etc.). Esto se puede solucionar creando una clase con la siguiente firma y ejecución donde la clase se invoca a ella misma:

Esta clase permite, gestionar un gran volumen de datos, mediante la ejecución iterativa, y además puede ser invocada mediante una llamada Cron.

7. Se requiere procesar N millones de registros (siendo N<50 millones)

Existen varias opciones, y enumero el orden de mi preferencia:

Opción 1: una clase que implemente la interfaz Batchable. Esta es mi opción preferida, porque es la más simple (KISS), y deja en manos de la plataforma la creación de los procesos internos reportando de forma centralizada los resultados en el ApexAsyncJob en una sola entrada. Es fácil reportar, analizar, y diagnosticar, pero puede no ser la más eficiente.

Opción 2: Alternativamente  crear una clase que implemente la interfaz Batchable, pero con esquema de ejecución distinto.

  • Aquí enviamos a la Flex Queue 5 instanciaciones de la clase con un volumen proporcional para cada instancia, alrededor del 20% en cada una.
  • Dado que la plataforma soporta hasta 100 procesos Batch en estado Holding, y 5 procesos en Activo (esto es Queued, Preparing ó Processing) conseguimos la máxima paralelización de los procesos.

Existen varios inconvenientes para que este esquema no sea mi opción preferida:

    1. La monitorización del proceso está dividida en 5 procesos técnicos
    2. Podemos provocar, o seguro provocaremos retardo en el resto de procesos asíncronos de la ORG, dado que consumimos los 5 slots posibles (por supuesto disminuir ese número alivia esta situación) para procesos activos
    3. Un acceso tan concurrente sobre un objeto/s provocará bloqueos en en el caso de operaciones de DML (debe valorarse en cada caso el controlar la ordenación de los registros por clave, etc.) que pueden ralentizar los tiempos de ejecución

Como opción 3: implementación de una clase con la interfaz Queueable mediante el uso de auto-Chaining, aplicando el método comentado anteriormente. Por supuesto el proceso es mucho más lento, pero al ejecutarlo secuencial por paquetes, podemos obtener ciertos beneficios de menor concurrencia, control temprano de errores, etc.

Como opción 4: otra alternativa con mejor rendimiento, consiste en un esquema de ejecución tal que, lanzar tantos Jobs de esta clase como conjuntos de datos querramos. Es decir, nuevamente encolamos 5 o N ejecuciones de esta clase. En el momento en que la plataforma disponga de recursos esos Jobs se ejecutarán en paralelo hasta un grado máximo de 5.

  • Esta implementación puede ser apetecible para usar Queueable (evitar cambios de código) y segmentar el proceso en N jobs. Eso si, la segmentación de los datos y encolamiento requiere una preparación previa, pero no modificar el código existente.

8. Se requiere procesar N millones de registros (siendo N>=50 millones)

Las alternativas son las mismas que el apartado anterior, pero creando tantos procesos de tipo Batchable o Queueable que conformen el volumen total, sin superar los límites de la plataforma.

Aunque la documentación oficial alienta al uso de Jobs Batchable, que también es mi opción preferida, un Chaining con Queueable en sus  variantes anteriores también consigue el procesamiento de grandes volúmenes de datos.

Mi preferencia sigue siendo el uso de varios Batchable y dejar que la plataforma gestione el job de forma completa.

9. Se requiere el máximo rendimiento sin afectación ninguna sobre el rendimiento de los procesos existentes

Mi opción preferida obliga al uso de recursos externos, y creo que idealmente usando Heroku. Cualquier otra IaSS o PaSS requiere de movimiento de datos que descartan un buen rendimiento excepto si estuviéramos usando Salesforce Connect para acceso a esos datos y la ejecución la lleváramos a cabo en la plataforma externa.

Heroku y Heroku Connect poseen capacidades no presentes en Force.com:

  • podemos aumentar/disminuir la capacidad de computación en base a nuestras necesidades o a condiciones circunstanciales puntuales
  • podemos modificar la funcionalidad sin necesidad de cambios en la ORG (desplegamos nuevas versiones de nuestro código en Heroku)
  • mantenemos la capacidad de cambios en los datos bidireccionalmente
  • programamos en el lenguaje y utilizamos las herramientas de desarrollo que dominamos

Tiene un coste asociado dado que la ejecución se realiza fuera de la plataforma y se usan recursos en Heroku.

10. Se requiere ejecutar un Job al cabo de 6 horas de su última ejecución (que no es lo mismo que un Job se ejecute cada 6 horas)

Existen varias opciones. La opción 1: consiste en Implementar un Job con la interfaz Schedulable conjuntamente con Queueable/Batchable.

  1. En caso de Queueable la última instrucción del método execute será una llamada a System.scheduleBatch con una expresión CRON que sea del momento actual + 6 horas.
  2. En el caso de Batchable lo haremos en la última línea del método finish.

La opción 2 consiste en que, al finalizar el proceso, lanzar un Platform Event que un Trigger recojerá para ejecutar el mismo System.scheduleBatch comentado.

Esta opción, aunque parece más rebuscada, permite un tratamiento de datos o coordinación con otros procesos antes de la planificación que nos pueda interesar.

11. Se requiere la ejecución condicionada de un Job sólo si otros procesos «ancestros» se han ejecutado previamente

Un ejemplo: el proceso de envío de nóminas solo debe ejecutarse, si previamente han finalizado 2 procesos: (1) el de cálculo de Bonus por ventas realizadas y (2) el de cálculo compensaciones familiares.

Inicialmente podríamos pensar que encolando 3 procesos de forma secuencia sería suficiente, se ejecutarán en secuencia. Este es un error común que no recuerda que la plataforma puede ejecutar hasta 5 procesos concurrentes. Por tanto necesitamos, un «coordinador» que compruebe la ejecución de los ancestros, previa ejecución del proceso dependiente.

No considero una opción válida la planificación del proceso dependiente, validando en su método constructor si sus padres se han ejecutado, y ejecutarse en caso positivo o replanificarse en caso negativo, ya que es un Prueba-Error con consumo innecesario de recursos.

No es una opción óptima el encadenamiento de los 3 procesos mediante Chaining, dado que los procesos de tickets de comida y de cálculos familiares pueden ejecutarse en concurrencia, lo que supone no aprovechas los recursos del sistema.

En general, la ejecución condicionada  de un Job a la finalización de otros procesos, se resuelve correctamente con:

  1. Lanzamiento de eventos de plataforma avisando de finalización del proceso
  2. Se introducen Triggers que escuchan estos Eventos y ejecutamos lógica de validación de las condiciones mediante el acceso a AsyncApexJob, donde encontramos el historia de ejecuciones de los procesos asíncronos.
  3. Cuando el último de los procesos finaliza, la lógica comprobará que en ApexAsyncJob todos los procesos ancestros han sido ejecutados, e incluso podemos validar su finalización, y ejecutar el proceso hijo.

Esta solución que solventa el caso de uso, se convierte en una maraña de Triggers y procesos de control cuando son muchos los procesos y varias las condiciones a cumplir.

Por ello,  IMHO ya tenemos las primeras señales de la necesidad que nos acercamos a los límites funcionales de las capacidades de planificación de la plataforma y debemos abordar el diseño de un planificador dinámico.

Casos de uso no cubiertos por la Plataforma

Casos de Async Apex no cubiertos por la plataforma – Homer y yo le damos vueltas al tema

Supongamos que debemos ejecutar el proceso X a las 3.00 de la mañana con las máximas garantías de ejecución (es decir, debemos garantizar que a las 3.00, al menos queda 1 slot libre de los 5 disponibles) cuando tenemos otros tantos procesos en posible ejecución

  • Aunque podemos preparar un calendario para intentar asegurar que a las 3.00 hora no tengamos 5 procesos activos, esta no es la situación idónea, dado que por circunstancias X pueda ser que no se cumpla. Si somos muy conservadores, estaremos desaprovechando capacidades de la plataforma.
  • Por ello, aquí detectamos la necesidad de establecer un control de las ejecuciones, de sus dependencias y sus condiciones de ejecución, para controlar el flujo de ejecución, conocer el estado de la plataforma, y con toda esta información construir un planificador dinámico propio que tome la decisión de creación o no de los procesos que estén parametrizados.

Si has llegado hasta aquí, te mereces un premio!!

Conclusiones

Las capacidades asíncronas y el rendimiento obtenido en Salesforce son excelentes, y nos permiten delegar en recursos especializados tareas que podrían comportar problemas de rendimiento para el usuario online.

Por experiencia propia, es importante notar que estos procesos se ejecutan cuando la plataforma tiene recursos disponibles, que es muy a menudo, pero no debemos diseñar pensando que tendremos ejecuciones en cierto momento, con cierto tiempo de respuesta, porque en algunos casos puede ser una experiencia frustrante (recordemos que estamos en un entorno multi-tenant).

En términos generales, mi preferencia es inicialmente la implementación  de la interfaz Batchable, dadas sus capacidades y simplicidad de uso, reporting y seguimiento incluso combinándola con Schedulable para programar su ejecución.

El uso de la anotación Future, está circunscrito a casos muy concretos o limitar el refactoring del código para implementar una interfaz, que en la mayoría de los casos no es complejo, y en cambio aporta unas capacidades muy superiores.

El concepto de Chaining es poderoso, y esquemas de ejecución como el de Scott, aportan ideas para estrujar la plataforma mediante código elegante y mantenible.

Como no, la siguiente entrada será un ejemplo de un Planificador Dinámico que da respuesta al caso de uso  que hemos visto, que la plataforma no cubre, pero eso lo veremos un poquito más adelante.

Por favor, no dudes en enviarme tus comentarios, opinar sobre los casos de uso, sobre mis preferencias, etc., es muy enriquecedor obtener los puntos de vista de otras personas.

Anuncio publicitario

2 comentarios sobre “Casos de uso para Apex asíncrono

  1. Genial como siempre Esteve!!

    Una duda que tengo a ver si me la puedes resolver… en un trailhead he leído (y mi entendimiento de siempre ha sido este) que en entorno de test, puedes ejecutar un proceso batch que tenga sólo un lote (usando test.startTest y test.stopTest). Sin embargo en la documentación de límites pone que puedes lanzar hasta 5 procesos batch en entorno de test. ¿Significa esto que podría lanzar hasta 5 procesos batch, pero sólo con 1 lote cada uno, usando test.startTest y test.stopTest?

    Y otra dudilla. También he leído por ahí que el orden de los lotes que el proceso batch ejecuta no está garantizado, es decir, que aunque tú le pases los registros a procesar en un orden determinado (con el query locator), podría ser que se ejecutaran en otro orden, no FIFO. ¿Te has encontrado con este problema alguna vez? Porque yo siempre que ejecuto procesos batch me da la sensación de que el orden se respeta, aunque salesforce no lo garantice.

    Gracias!!!!

    Le gusta a 1 persona

Deja una respuesta

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Salir /  Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Salir /  Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Salir /  Cambiar )

Conectando a %s

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.