Este texto se tradujo utilizando el sistema de traducción automatizado de Salesforce. Realice nuestra encuesta para proporcionar comentarios sobre este contenido e indicarnos qué le gustaría ver a continuación.
Las arquitecturas modernas de Salesforce están cada vez más impulsadas por el procesamiento asíncrono; no como una comodidad, sino como un requisito estratégico para la escala. En los últimos años, hemos visto a más y más compañías lidiar con volúmenes de datos crecientes, integraciones complejas que implican múltiples puntos de contacto y el auge de sistemas autónomos que se ejecutan 24/7/365. Todo esto empuja a los arquitectos hacia el diseño de sistemas que son asíncronos en primer lugar.
El procesamiento asíncrono en Salesforce a menudo significa diseñar alrededor de límites reguladores y complejidad. Esos límites actúan como barandillas y restricciones arquitectónicas que ayudan a producir sistemas escalables y seguros en masa. Aunque ningún límite de plataforma sirve directamente para gestionar la complejidad, los patrones de diseño pueden ayudar a mitigar el riesgo en ese frente. Internamente, Salesforce a menudo amplía los límites de la plataforma para probar nuevas funciones y automatizar procesos de negocio complejos. Construimos un Marco de trabajo de procesamiento asíncrono basado en pasos para ejecutar trabajos asíncronos con un número arbitrario de pasos. Cada paso puede ejecutarse, reintentarse y reiniciarse de forma independiente con controles de gobernanza compartidos y visibilidad operativa completa a través del registro centralizado. Este documento describe sus componentes arquitectónicos clave: Apex y finalizadores colocables en cola, Flujo programado, Cursores Apex, Acciones invocables e integraciones con Slack. Juntos, estos componentes proporcionan una arquitectura modular, ampliable y observable adecuada para necesidades de negocio en evolución.
- Las arquitecturas modernas de Salesforce deben adoptar un enfoque asíncrono primero para alcanzar la escala, la resiliencia y la transparencia operativa.
- Dividir el trabajo complejo en pasos ejecutables de forma independiente permite un desempeño predecible, reintentos más seguros, puntos de control, reversión y evolución modular sin volver a diseñar flujos de trabajo principales.
- El marco de trabajo proporciona una alternativa ampliable a trabajos por lotes monolíticos y antiguos, llamadas asíncronas encadenadas y flujos profundamente anidados, y está construido para cargas de trabajo de gran volumen que deben ampliarse horizontalmente dentro de Salesforce sin orquestación fuera de la plataforma.
- La ejecución determinista y observable garantiza el seguimiento del progreso, el monitoreo de SLA, el diagnóstico de fallos y la transparencia a nivel de auditoría a través del registro y la gobernanza centralizados.
- Diseñado para el rigor de nivel de compañía, incluyendo la gobernanza unificada, el cumplimiento y el control de estado distribuido entre procesos de negocio de larga duración.
Antes de revisar los requisitos, estos son algunos de los aspectos que se deben y no se deben tener en cuenta para cuándo utilizar un marco como este. Sobre todo, considere qué sistema es la única fuente de verdad. Si su organización de Salesforce depende mínimamente de datos externos pero necesita ampliarse de cientos a millones de registros, considere un marco de trabajo asíncrono basado en pasos.
Sí utilice este marco de trabajo si:
- La mayoría (o toda) la información sobre la que actuar ya existe en su CRM.
- El costo inicial o continuo de mantener un trabajo Extraer carga de transformación (ETL) para armonizar datos externos es demasiado alto.
- Debe aplazar el procesamiento de un gran número de registros de Salesforce en una programación establecida.
- Puede desglosar el procesamiento en pasos discretos. Por ejemplo, puede crear un conjunto de registros jerárquicos o basados en árboles, especialmente si el volumen de datos se desvía de la jerarquía o el árbol.
No utilice este marco de trabajo si:
- La creación o actualización de registros requiere un nuevo cálculo inmediato.
- La integración es un reto porque los sistemas externos alojan datos principales para actualizaciones de registros. (Considere el envío de datos actualizados a Salesforce con la API masiva.)
Con esas prácticas en mente, revisemos nuestros requisitos y comencemos a construir.
Considere la declaración de problema:
Dado un trabajo que necesita ejecutarse diariamente, compruebe si ciertos registros cumplen criterios preestablecidos para un procesamiento posterior. Si lo hacen, inicie esos trabajos de procesamiento. Procesar registros podría significar extraer datos de múltiples sistemas externos para realizar cálculos. Los pasos en trabajos deben notificar a las personas a través de Slack que los registros procesados están listos para su revisión. Los pasos también deben distribuir notificaciones a gestores y superiores en la jerarquía de funciones basándose en un retraso configurable después de la primera ronda de notificaciones.
Este problema implica varios pasos diferentes, algunos de los cuales pueden producirse independientemente entre sí. Existen muchas formas de dividir el trabajo. Esta es una agrupación:
- El programador.
- La interfaz de pasos e implementaciones concretas que procesan registros (independientemente del tipo de procesamiento).
- El procesador que organiza los pasos.
- El Apex Invocable llamado por el programador.
- La pieza de notificación. Utilizamos el SDK Apex Slack.
- Existe cierta complejidad oculta en la frase “retraso configurable”. Revisaremos esta complejidad más adelante en este artículo.
Este es un diagrama con opiniones para el marco de trabajo construido:
Ahora, desglose ese diagrama y comience a construir las piezas.
Flujo programado ofrece varias ventajas como mecanismo de programación:
- Los flujos programados se pueden empaquetar e implementar como metadatos. Esto no es cierto para trabajos programados mediante Apex (o a través de la página Trabajos programados).
- El elemento Espera es crítico para marcos de trabajo que requieren llamadas. Al utilizarlo en Flujo, las llamadas no son necesarias en la parte invocable del marco de trabajo.
- La granularidad de la programación cumple los requisitos: el intervalo mínimo para Flujos programados es diario. Si necesita una frecuencia más alta (por ejemplo, cada hora), reconsidere Flujo programado para este requisito.
Otra consideración al configurar el Flujo programado es la apertura de puertas de entorno. Antes de invocar la acción Apex, agregue un elemento Decisión que evalúe la variable {!$Api.Enterprise_Server_URL_100}. Esto garantiza que el trabajo se ejecute solo en los entornos previstos, como UAT y Producción. Este patrón es importante porque los entornos sandbox se actualizan con frecuencia o se crean recientemente durante el SDLC, y sin una comprobación de entorno explícita, un Flujo programado podría ejecutarse de forma no intencionada en entornos donde el marco no está destinado a ejecutarse. El uso del operador contains en el elemento Decisión hace que la configuración sea resistente a futuras creaciones de sandbox o cambios de URL.
Finalmente, considere cómo debe capturar el marco los fallos. Agregue siempre una ruta de fallo cuando Flow llame a cualquier acción; por ejemplo, puede transferir fallos a la acción "Agregar entrada de registro" de Nebula Logger. Nebula Logger escribe registros en objetos personalizados, de modo que los clientes deben saber que los datos de registro consumen almacenamiento de la organización; de forma predeterminada, los registros se almacenan durante 14 días en una organización y luego se limpian; este periodo de retención es configurable. Nebula Logger también utiliza Eventos de plataforma para publicar registros, de modo que las entradas de registro se guardan independientemente de la transacción de procesamiento de datos principal; esto garantiza la captura de fallos incluso si la acción principal Flujo o Apex se revierte. Los clientes deben evaluar el volumen de registro esperado y los requisitos de retención cuando consideren la incorporación de un marco de trabajo de registro.
Este es el aspecto del flujo:
Pasemos a los primeros fragmentos de código Apex con el requisito de programación ahora satisfecho.
Definir una interfaz de Step:
Para este artículo, la interfaz de step se muestra como una clase externa para mayor claridad. El marco en sí es flexible: los equipos pueden organizar la interfaz y sus implementaciones utilizando cualquier patrón de empaquetado Apex que prefieran, siempre que todas las clases Paso hagan referencia a la misma interfaz.
Existen algunas cosas a tener en cuenta acerca de los métodos definidos en nuestra interfaz:
execute, aunque sin argumentos por el momento, mejora cuando pasamos una clase deState(o interfaz) para orquestar datos entre pasos cuando el pedido importa.- Los
getNamepodrían devolver un valor deSystem.Typeen vez de unString. El objetivo es proporcionar a la capa de orquestación una forma de registrar nombres de pasos sin exponer otras propiedades.
Esta es la primera implementación concreta que muestra cómo se ajustan estas piezas. Con una excepción más adelante, recomendamos utilizar Apex colocable en cola para implementar el procesamiento asíncrono en Apex; Apex por lotes es normalmente innecesario (y se desaconsejan los métodos de @future). Apex colocable en cola se inicia rápidamente y, con Apex Cursors, tiene muchas ventajas sobre Apex por lotes.
Los cursores Apex ofrecen una alternativa moderna al modelo Apex por lotes tradicional. Al igual que el procesamiento por lotes, una implementación de Cursor puede obtener registros en partes (hasta 2.000 por lote). Sin embargo, los Cursores permiten múltiples recuperaciones en una sola transacción, lo que permite un rendimiento significativamente superior para operaciones de gran volumen.
Al adoptar Cursores como parte de este marco de trabajo, los equipos deben tener en cuenta las limitaciones de prueba y simulabilidad actuales. El comportamiento del cursor en las pruebas puede diferir del comportamiento de producción, por lo que es importante diseñar estrategias de prueba que eviten depender de los elementos internos del cursor y que validen la lógica de orquestación en los límites. A medida que evolucione la plataforma, estas áreas seguirán mejorando, pero la orientación principal sigue siendo: Los cursores proporcionan un desempeño superior y una carga de orquestación reducida en comparación con Apex por lotes para muchos casos de uso.
Para definir un límite claro entre el Cursor proporcionado por el sistema y su propio código, recomendamos crear una representación similar a Cursor al implementar la interfaz de Step. Considere este código:
Observe la clase de Cursor. Los cursores Apex son instancias de Database.Cursor, pero nuestra implementación de Cursor nos da flexibilidad sobre las deficiencias de Cursores. Esta es la implementación:
Para el resto de este artículo, omitimos las declaraciones de sharing al hacer referencia a clases Apex. En la práctica, asegúrese de que las clases de nivel superior se utilizan explícitamente con o sin colaboración para ajustarse a su modelo de objeto y permisos.
Tenga en cuenta también que nuestra implementación de Cursor delega en el Database.Cursor de plataforma, con beneficios adicionales discutidos a continuación.
En primer lugar, estas son las pruebas correspondientes:
Al hacer que las Cursor sean virtuales, las implementaciones de CursorStep concretas pueden operar sin un Database.Cursor cuando no necesitan iterar un conjunto de registros grande, similar a devolver un System.Iterable<T> en vez de un Database.QueryLocator en Apex por lotes. Este es un ejemplo:
Tenga en cuenta que como esta clase también es abstracta, deja la implementación concreta de innerExecute a subclases.
También existe una alternativa a la subclase interna CursorLike. Si sabe que las versiones concretas de un paso como este no superarán otros límites reguladores, puede devolver this.records desde CursorLike.fetch y sustituir la CursorStep.shouldRestart() principal para devolver false. Eso le permite iterar sobre una lista limitada solo por el límite de pila de Apex de 12 MB por transacción asíncrona.
Nuestra implementación basada en cursores nos proporciona mucha flexibilidad al paginar grandes cantidades de datos. La interfaz de Step, por su parte, nos proporciona la flexibilidad de describir y encapsular pasos de todo tipo.
Considere un paso basado en flujo:
Como los flujos no pueden devolver parámetros de salida que se ajusten a un tipo definido por Apex, comprobamos si hay un parámetro de salida de shouldRestart antes de utilizarlo.
Algunos pasos podrían estar marcados con funciones. Puede implementar lógica para decidir qué pasos incluir o utilizar un paso sin operación para una función desactivada. El patrón Objeto nulo es una forma común de reducir la complejidad en la capa de orquestación:
Ahora tenemos bastantes elementos constructivos con los que trabajar. Veamos la capa de orquestación responsable de iterar sobre pasos.
El procesador es un punto de inflexión en la arquitectura. Debemos decidir quién define qué pasos se inicializan y dónde. Las opciones incluyen:
- Haga que el procesador defina qué pasos asignar a la lógica de negocio. Esta opción es sencilla, pero se amplía poco para facilitar la lectura.
- Defina la asignación con Metadatos personalizados (CMDT). Los campos Relación de metadatos no admiten
ApexClass, lo que combina la ortografía de nombres de clases en su configuración de procesos de negocio. Puede reducir el riesgo de administrador convirtiendo el campo en una lista de selección y validando el tipo que existe (Type.forName()o consultandoApexClass), pero como los registros CMDT no admiten desencadenadores, la validación se produce en tiempo de ejecución. Esta ruta es comprobable, pero los administradores aún pueden crear registros CMDT solo en producción: continúe con cuidado. - Defina la asignación con registros. Los no administradores pueden configurar pasos, pero las implementaciones se vuelven más difíciles y los entornos pueden desviarse. Proceda con precaución.
Existe una cita famosa de Clean Code acerca de cómo gestionar esta complejidad en particular:
La solución a este problema es enterrar la declaración
switch[para crear objetos] en el sótano de una fábrica abstracta, y nunca dejar que nadie la vea.
Teniendo esto en cuenta, y como nuestro número actual de pasos está bien definido y es poco probable que crezca demasiado, está bien que el procesador de pasos sea también la fábrica de pasos. Esto puede utilizar una enumeración para dirigir la declaración switch:
Y luego para nuestro StepProcessor:
Los métodos de fábrica mostrados, como addTypeOneSteps(), pueden delegar preocupaciones como el marcado de funciones; cleanSteps() realiza una comprobación puntual en los pasos recopilados para asegurarse de que no hay ningún paso “vacío” antes de ser verdaderamente asíncrono. Podría tener este aspecto:
No hemos debatido el tratamiento de errores desde que mencionamos el Registrador de nebulosas en la sección Flujo programado. Esto se debe a que la System.Finalizer nos permite cubrir el registro de todas las condiciones de error sin agregar la gestión de errores específica en cada paso. Cada Step se centra en la ejecución, mientras registramos y volvemos a lanzar cualquier ruta insatisfecha de modo que aflore en pruebas de unidad. Esto admite la iteración segura y las alertas a nivel de producción (utilizando el complemento Slack Logger para Nebula para todos los registros WARN y ERROR).
Una nota acerca del registro de errores: pasar la instancia del paso a mensajes de registro asume un nivel de Trust en lo que se hace visible en los registros. El toString() predeterminado para clases Apex incluye todas las propiedades estáticas y a nivel de instancia en el mensaje. Eso puede ser deseable, o puede filtrar información confidencial. Aunque el registro y la seguridad no son el enfoque aquí, tenga en cuenta que para algunos sistemas, la adhesión a una interfaz como Step también puede implicar forzar una sustitución para toString().
Este método impone a cada creador de objetos la responsabilidad de decidir qué se puede imprimir, lo que puede ser deseable.
En niveles de registro: a nivel de StepProcessor, utilizamos INFO, el nivel de no error más alto. A medida que se vuelve más granular en la aplicación, los niveles de registro deben disminuir en consecuencia. Los pasos individuales podrían utilizar DEBUG para información de alto nivel, con FINE, FINER y FINEST reservados para resultados cada vez más detallados. El registro es tanto un arte como una ciencia, pero seguir estos principios ayuda a mantener los registros coherentes y útiles.
Antes de continuar, reflexionemos brevemente sobre la decisión de que nuestro procesador de pasos aloje la lógica para la que se utilizan los pasos. En una base de código grande, considere hacer que los StepProcessor sean virtuales o abstractos y haga que las subclases identifiquen pasos específicos para establecer una separación apropiada de las preocupaciones.
El programador eventualmente invoca Apex. Con el resto de la configuración completa, la sección Apex invocable puede decidir qué pasos se deben ejecutar y pasar el List<StepType> al procesador:
Esta es una parte sencilla de la ecuación: el uso de registros, datos o lógica para determinar qué tipos de pasos ejecutar. La acción invocable es sencilla porque encapsulamos la complejidad en otro lugar. También nos hemos protegido contra excepciones inesperadas y hemos hecho que cada pieza sea fácil de probar de forma aislada.
El SDK Apex Slack está más allá del ámbito de este artículo, pero un posible inconveniente de los requisitos merece una revisión: notificar a las personas en la jerarquía de funciones basándose en un retraso configurable. Sobre el papel, esto es sencillo y es posible que considere (correctamente) System.enqueueJob(this) en la StepProcessor. Con System.AsyncOptions, nuestra inclinación inicial era utilizar la sobrecarga de enqueueJob para satisfacer este requisito.
Por ahora, sin embargo, la demora máxima a través de System.AsyncOptions.MinimumQueueableDelayInMinutes es de 10 minutos. Como el requisito es de 120 minutos, quedan algunas opciones. Un enfoque ingenuo podría tener este aspecto:
En la práctica, el retraso se pasaría a esta clase porque el retraso está dirigido por la configuración.
No recomendamos este enfoque a menos que esté seguro de que solo habrá un tipo de notificación retrasada. Se quema en 11 trabajos asíncronos adicionales antes de comenzar (o más, si el retraso aumenta). Ese costo podría estar bien para un trabajo, no para muchos. También necesitaría agregar un método a la interfaz de Step de modo que cada paso pueda indicar al procesador cuánto tiempo esperar antes de reiniciar, lo que agrega ruido.
Eso nos deja dos posibilidades interesantes:
- Puede dividir el paso retrasado en su marco de trabajo existente si ya tiene un trabajo de sondeo programado en un intervalo apropiado. También debería estar de acuerdo con que el retraso especificado llegue hasta 15 minutos después (15 minutos es el intervalo de actualización mínimo para una expresión de CRON programada por Apex). Esto coincide aproximadamente con el ejemplo de Apex invocable; la programación se realiza mediante Apex en su lugar. En otras palabras, puede reutilizar la misma arquitectura basada en
Steppara procesar registros basándose en una marca de tiempo “Iniciar después” y decidir qué pasos utilizar basándose en una lista de selección o asignación de lista de selección múltiple a los valores de enumeración deStepTypemostrados anteriormente. - De manera alternativa, si está cómodo definiendo una clase de Apex externa adicional, vuelva a Apex por lotes (a diferencia de Apex colocable en cola, que admite clases internas, las clases Apex por lotes deben ser clases externas) utilizando
System.scheduleBatch().
Considere el ejemplo de Apex por lotes. Aunque generalmente recomendamos Apex colocable en cola por flexibilidad y control, este es un caso donde Apex por lotes aún reina:
Luego, en la StepProcessor, imagine que el método de addTypeOneSteps() mostrado anteriormente se actualiza con este paso retrasado:
Aunque normalmente no recomendamos esta cantidad de saltos, este retraso de paso se convierte en otro elemento constructivo reutilizable. Hasta que se permitan demoras más largas en Apex colocable en cola, también representa la forma más sencilla de producir este efecto (sin un mecanismo de sondeo, como se discutió).
Utilizamos el diseño orientado a objetos para cumplir los requisitos y creamos un sistema que se ampliará mientras equilibra el costo a largo plazo de la construcción y el mantenimiento. Aunque la declaración de pasos y la creación de instancias pueden en última instancia superar su lugar en StepProcessor, hay poca deuda técnica adicional aquí. Con FlowStep, los administradores y desarrolladores pueden decidir juntos cuándo tienen más sentido las soluciones sin código o procódigo.
Utilizando la interfaz de System.Finalizer en el marco de trabajo Colocable en cola de Apex, junto con Nebula Logger, construimos un sistema sólido y comprobable que nos alerta de fallos imprevistos incluso si los pasos futuros carecen de registro explícito. Para nosotros, este sistema está felizmente ajustando números y reduciendo costos y complejidad. También nos ha proporcionado perspectivas valiosas sobre el comportamiento de los Cursores Apex bajo cargas de trabajo reales, ayudándonos a perfeccionar nuestro enfoque mientras mejoramos la función en sí.
Al descomponer cargas de trabajo complejas y de gran volumen en pasos de ejecución modulares, el marco de trabajo de procesamiento asíncrono basado en pasos transforma las restricciones de plataforma en ventajas de ingeniería, permitiendo desempeño, observabilidad y gobernanza predecibles a escala de la compañía. Los pasos pueden configurarse por administradores y desarrolladores, y en cualquier caso, los autores de pasos pueden centrarse de forma segura en la observación de los límites reguladores de plataforma básicos (como filas DML y filas de consulta recuperadas) sin tener que preocuparse por cómo ampliar cada paso.
Para poner en marcha y adoptar este patrón en implementaciones de negocio, los arquitectos deben:
- Evalúe automatizaciones existentes para identificar áreas donde la orquestación asíncrona puede ayudar a mejorar el desempeño y la observabilidad.
- Divida procesos grandes en pasos discretos ejecutables de forma independiente con objetivos de procesamiento claros y puntos de autor discretos (como Flujo o Apex).
- Defina y agrupe tipos de pasos para acelerar la estandarización y la reutilización de pasos entre unidades de negocio.
- Pilote el enfoque con nuevos procesos o automatizaciones existentes. ¡Es posible que se sorprenda al descubrir cuántos casos Edge encontrará de forma gratuita en cuestión de pasos, cuidando su registro integrado y observabilidad!
James Simone es ingeniero de software principal en Salesforce y tiene más de una década de experiencia trabajando en la plataforma. Era cliente de Salesforce (y propietario de productos) antes de pasar al desarrollo, y ha estado redactando inmersiones técnicas profundas sobre Salesforce desde 2019 en The Joys Of Apex. Anteriormente publicó artículos en el blog Salesforce Developer y también en el blog Salesforce Engineering.