Ce texte a été traduit en utilisant le système de traduction automatisé de Salesforce. Répondez à notre sondage pour nous faire part de vos commentaires sur ce contenu et nous dire ce que vous aimeriez voir ensuite.

Les architectures Salesforce modernes sont de plus en plus pilotées par le traitement asynchrone, non pas comme une commodité, mais comme une exigence stratégique d'échelle. Ces dernières années, nous avons vu de plus en plus d'entreprises faire face à des volumes de données en augmentation, à des intégrations complexes impliquant plusieurs points de contact et à l'essor des systèmes autonomes fonctionnant 24/7/365. Toutes ces choses poussent les architectes à concevoir des systèmes qui sont d'abord asynchrones.

Le traitement asynchrone dans Salesforce implique souvent de concevoir en fonction des limites et de la complexité du gouverneur. Ces limites agissent comme des garde-fous et des contraintes architecturales qui aident à produire des systèmes évolutifs et sûrs en masse. Bien qu'aucune limite de plate-forme ne serve directement à gérer la complexité, les modèles de conception peuvent aider à atténuer les risques sur ce front. En interne, Salesforce repousse souvent les limites de la plate-forme pour tester de nouvelles fonctionnalités et automatiser des processus métiers complexes. Nous avons élaboré une infrastructure de traitement asynchrone basée sur une étape pour exécuter des tâches asynchrones avec un nombre arbitraire d'étapes. Chaque étape peut être exécutée, réessayée et redémarrée indépendamment avec des contrôles de gouvernance partagés et une visibilité opérationnelle totale via la consignation centralisée. Le présent document décrit ses principaux éléments architecturaux : Apex et finaliseurs Queueable, flux planifié, curseurs Apex, actions invocables et intégrations à Slack. Ensemble, ces composants offrent une architecture modulaire, évolutive et observable adaptée à l'évolution des besoins de l'entreprise.

  • Les architectures Salesforce modernes doivent adopter une approche asynchrone avant tout pour atteindre l'échelle, la résilience et la transparence opérationnelle.
  • La division d'un travail complexe en étapes exécutables indépendantes permet d'obtenir des performances prévisibles, des tentatives plus sûres, un pointage, une restauration et une évolution modulaire sans reconcevoir les workflows de base.
  • L'infrastructure offre une alternative évolutive aux tâches par lot monolithiques et vieillissantes, aux appels asynchrones enchaînés et aux flux profondément imbriqués, et est conçue pour les charges de travail à haut volume qui doivent évoluer horizontalement dans Salesforce sans orchestration hors plate-forme.
  • Une exécution déterministe et observable garantit le suivi des progrès, la surveillance des accords de niveau de service, les diagnostics d'échec et la transparence au niveau de l'audit grâce à une consignation et une gouvernance centralisées.
  • Conçu pour la rigueur de l'entreprise, notamment la gouvernance unifiée, la conformité et le contrôle de l'État distribué à travers des processus métiers à long terme.

Avant de passer en revue les exigences, voici quelques recommandations à ne pas faire pour utiliser une infrastructure de ce type. Réfléchissez surtout au système qui est la source unique de vérité. Si votre organisation Salesforce s'appuie peu sur des données externes, mais doit passer de centaines à des millions d'enregistrements, envisagez une infrastructure asynchrone basée sur une étape.

**N'**utilisez cette infrastructure que si :

  • La plupart (ou toutes) les informations sur lesquelles agir existent déjà dans votre CRM.
  • Le coût initial ou permanent de la maintenance d'une tâche ETL (Extract Transform Load) pour harmoniser les données externes est trop élevé.
  • Vous devez reporter le traitement d'un grand nombre d'enregistrements Salesforce selon une planification définie.
  • Vous pouvez décomposer le traitement en étapes discrètes. Par exemple, vous pouvez créer un ensemble d'enregistrements hiérarchiques ou arborescents, en particulier si le volume de données s'étend à la hiérarchie ou à l'arborescence.

**N'**utilisez pas cette infrastructure si :

  • La création ou la mise à jour d'enregistrements nécessite un recalcul immédiat.
  • L'intégration est difficile, car les systèmes externes hébergent les données principales pour les mises à jour d'enregistrements. (vous pouvez envoyer automatiquement des données mises à jour à Salesforce avec l'API de transfert en masse).

Avec ces pratiques à l'esprit, révisons nos exigences et commençons à élaborer.

Tenez compte de l'énoncé du problème :

Avec une tâche qui doit être exécutée quotidiennement, vérifiez si certains enregistrements remplissent des critères préétablis pour un traitement ultérieur. S'ils le font, lancez ces tâches de traitement. Le traitement d'enregistrements peut impliquer l'extraction de données de plusieurs systèmes externes pour effectuer des calculs. Les étapes des tâches doivent notifier les personnes via Slack que les enregistrements traités sont prêts à être révisés. Les étapes doivent également escalader les notifications aux responsables et aux échelons supérieurs de la hiérarchie des rôles en fonction d'un délai configurable après la première série de notifications.

Ce problème implique plusieurs étapes différentes, dont certaines peuvent se produire indépendamment les unes des autres. Il existe de nombreuses façons de répartir le travail. Voici un regroupement :

  • Le planificateur.
  • L'interface de l'étape et les implémentations concrètes qui traitent les enregistrements (quel que soit le type de traitement).
  • Le processeur qui organise les étapes.
  • L'Apex Invocable appelé par le planificateur.
  • L'élément de notification. On utilise le SDK Apex Slack.
    • L'expression « délai configurable » cache une certaine complexité. Nous allons examiner cette complexité plus loin dans cet article.

Voici un diagramme de l'infrastructure élaborée :

Diagramme architectural montrant Planificateur de flux, traitement asynchrone Apex et différentes implémentations d ' étapes possibles Maintenant, décomposez ce diagramme et commencez à construire les pièces.

Le flux planifié offre plusieurs avantages en tant que mécanisme de planification :

  • Les flux planifiés peuvent être empaquetés et déployés en tant que métadonnées. Ce n'est pas vrai pour les tâches planifiées via Apex (ou via la page Tâches planifiées).
  • L'élément Attente est essentiel pour les infrastructures qui nécessitent une légende. En l'utilisant dans Flux, les appels externes ne sont pas nécessaires dans la partie Invocable de l'infrastructure.
  • La granularité de la planification remplit les exigences : l'intervalle minimal pour les flux planifiés est quotidien. Si vous avez besoin d'une fréquence plus élevée (par exemple, toutes les heures), reconsidérez Flux planifié pour cette exigence.

Une autre considération lors de la configuration du flux planifié est le portage de l'environnement. Avant d'invoquer l'action Apex, ajoutez un élément Décision qui évalue la variable {!$Api.Enterprise_Server_URL_100}. Cela garantit que la tâche est exécutée uniquement dans les environnements prévus, tels que UAT et Production. Ce schéma est important, car les sandbox sont fréquemment actualisées ou créées pendant le développement de l'infrastructure, et sans vérification explicite de l'environnement, un flux planifié peut involontairement être exécuté dans des environnements où l'infrastructure n'est pas conçue pour être exécutée. L'utilisation de l'opérateur contains dans l'élément Décision rend la configuration résiliente aux futures créations de sandbox ou aux changements d'URL.

Enfin, déterminez comment l'infrastructure doit capturer les échecs. Ajoutez toujours un chemin de défaut lorsque Flux appelle n'importe quelle action. Vous pouvez par exemple câbler les défauts à l'action « Ajouter une entrée au journal » de Nebula Logger. Nebula Logger écrit des journaux dans des objets personnalisés. Par conséquent, les clients doivent savoir que les données des journaux consomment du stockage dans l'organisation. Par défaut, les journaux sont stockés pendant 14 jours dans une organisation, puis nettoyés. Cette période de rétention est configurable. Nebula Logger utilise également les événements de plate-forme pour publier des journaux. Par conséquent, les entrées dans les journaux sont enregistrées indépendamment de la transaction de traitement des données principale. Cela garantit la capture des échecs, même si l'action principale Flux ou Apex est annulée. Les clients doivent évaluer le volume de consignation attendu et les exigences de rétention en envisageant l'ajout d'une infrastructure de consignation.

Le flux se présente comme suit :

Le flux planifié contenant un élément Décision d ' exécution ou non, un élément Interrompre pour autoriser les appels externes, et l ' action Apex Invocable appelée avec le chemin d ' interruption de l ' Enregistreur Nebula

Passons aux premiers bouts de code Apex avec l'exigence de planification désormais satisfaite.

Définissez une interface de Step :

Pour cet article, l'interface de step est affichée en tant que classe externe pour plus de clarté. L'infrastructure elle-même est flexible. Les équipes peuvent organiser l'interface et ses implémentations en utilisant n'importe quel modèle d'empaquetage Apex de leur choix, à condition que toutes les classes Étape référencent la même interface.

Voici quelques points à noter concernant les méthodes définies dans notre interface :

  • La execute, bien que sans argument pour le moment, s'améliore lorsque nous passons une classe (ou interface) de State pour orchestrer les données entre les étapes lorsque l'ordre importe.
  • getName peut renvoyer une valeur de System.Type au lieu d'une String. L'objectif est de fournir à la couche d'orchestration un moyen de consigner les noms d'étape sans exposer d'autres propriétés.

Voici la première mise en œuvre concrète qui montre comment ces pièces s’agencent. À une exception près plus loin, nous recommandons d'utiliser Apex Queueable pour implémenter le traitement asynchrone dans Apex; Apex par lot est généralement inutile (et les méthodes de @future sont déconseillées). Apex Queueable démarre rapidement et présente, avec les curseurs Apex, de nombreux avantages par rapport aux Apex batch.

Les curseurs Apex offrent une alternative moderne au modèle Apex traditionnel par lot. De la même façon qu'un traitement par lot, une implémentation de curseur peut récupérer des enregistrements par segments (jusqu'à 2000 par lot). Cependant, les curseurs autorisent plusieurs récupérations dans une seule transaction, ce qui augmente considérablement le débit pour les opérations à grand volume.

Lors de l'adoption des curseurs dans ce cadre, les équipes doivent être conscientes des limitations actuelles en matière de test et de moquabilité. Le comportement du curseur dans les tests peut différer de celui de la production. Par conséquent, il est important de concevoir des stratégies de test qui évitent de dépendre des internes du curseur et valident à la place la logique d'orchestration aux frontières. À mesure que la plate-forme évoluera, ces domaines continueront de s'améliorer, mais les orientations principales demeurent les suivantes : Les curseurs offrent des performances supérieures et réduisent les surcharges d'orchestration par rapport à Apex par lot pour de nombreux cas d'utilisation.

Pour définir une frontière claire entre le curseur fourni par le système et votre propre code, nous recommandons de créer une représentation de type curseur lors de l'implémentation de l'interface de Step. Tenez compte du code suivant :

Notez la classe Cursor. Les curseurs Apex sont des instances de Database.Cursor, mais notre implémentation Cursor nous donne la flexibilité nécessaire pour contourner les lacunes des curseurs. Voici l'implémentation :

Pour la suite de cet article, nous omettons les déclarations de sharing en faisant référence à des classes Apex. En pratique, assurez-vous que les classes de niveau supérieur sont explicitement utilisées avec ou sans partage pour être conformes à votre modèle d'objet et à vos autorisations.

Notez également que notre implémentation Cursor délègue à la Database.Cursor de plate-forme, avec des avantages supplémentaires discutés ci-dessous.

Pour commencer, voici les tests correspondants :

En rendant les Cursor virtuelles, les implémentations CursorStep concrètes peuvent fonctionner sans Database.Cursor lorsqu’elles n’ont pas besoin d’itérer un ensemble d’enregistrements volumineux, de la même façon qu’en renvoyant un System.Iterable<T> au lieu d’un Database.QueryLocator dans Apex batch. Voici un exemple :

Notez que cette classe est également abstraite, elle laisse l'implémentation concrète de innerExecute à des sous-classes.

Il existe également une alternative à la sous-classe interne CursorLike. Si vous savez que les versions concrètes d'une étape comme celle-ci ne brûleront pas à travers les autres limites du gouverneur, vous pouvez renvoyer this.records depuis CursorLike.fetch et remplacer le CursorStep.shouldRestart() parent pour renvoyer false. Cela permet d'itérer sur une liste limitée uniquement par la limite en heap Apex de 12 Mo par transaction asynchrone.

Notre implémentation basée sur le curseur nous offre beaucoup de flexibilité lors de la pagination sur de grandes quantités de données. L'interface de Step, quant à elle, nous offre la flexibilité nécessaire pour décrire et encapsuler toutes sortes d'étapes.

Prenons une étape basée sur un flux :

Les flux ne peuvent pas renvoyer des paramètres de sortie conformes à un type défini par Apex. Par conséquent, nous vérifions la présence d'un paramètre de sortie shouldRestart avant de l'utiliser.

Certaines étapes peuvent être signalées par une fonctionnalité. Vous pouvez implémenter une logique pour décider des étapes à inclure ou utiliser une étape sans option pour une fonctionnalité désactivée. Le modèle Objet nul est un moyen courant de réduire la complexité dans la couche d'orchestration :

Nous avons maintenant plusieurs blocs de construction à travailler. Examinons la couche d'orchestration responsable de l'itération des étapes.

Le processeur est un point d'inflexion dans l'architecture. Nous devons décider qui définit les étapes à initialiser et où. Les options comprennent :

  • Demandez au processeur de définir les étapes à mapper avec une logique métier. Cette option est simple, mais son échelle est faible pour la lisibilité.
  • Définissez le mappage avec des métadonnées personnalisées (CMDT). Les champs Relation des métadonnées ne prennent pas en charge les ApexClass, qui couplent vaguement l'orthographe du nom de classe dans la configuration de vos processus métiers. Vous pouvez réduire le risque administrateur en définissant le champ en tant que liste de sélection et en validant l'existence du type (Type.forName() ou en interrogeant des ApexClass), mais comme les enregistrements CMDT ne prennent pas en charge les déclencheurs, la validation est effectuée à l'exécution. Cet itinéraire peut être testé, mais les administrateurs peuvent créer des enregistrements CMDT uniquement en production.
  • Définissez le mappage avec des enregistrements. Les non-administrateurs peuvent configurer des étapes, mais les déploiements deviennent plus difficiles et les environnements peuvent dériver. Procédez avec précaution.

Il y a une citation célèbre de Clean Code sur la façon de gérer cette partie particulière de complexité :

La solution à ce problème est d'enterrer l'instruction switch [pour fabriquer des objets] dans le sous-sol d'une usine abstraite, et de ne jamais laisser personne la voir.

Avec cela à l'esprit, et parce que notre nombre actuel d'étapes est bien défini et qu'il est peu probable qu'il augmente trop, il est correct que le processeur d'étape soit également l'usine pour les étapes. Cela peut utiliser une énumération pour piloter l'instruction switch :

Et puis pour notre StepProcessor:

Les méthodes d'usine affichées, par exemple addTypeOneSteps(), peuvent déléguer des problèmes tels que le marquage des fonctionnalités. cleanSteps() effectue un contrôle unique des étapes rassemblées pour s'assurer qu'il n'y a pas d'étapes « vides » avant la synchronisation. Ça pourrait ressembler à ça :

Nous n'avons pas abordé le traitement des erreurs depuis que nous avons mentionné Nebula Logger dans la section Flux planifié. En effet, System.Finalizer nous permet de couvrir la consignation de toutes les conditions d'erreur sans ajouter de traitement spécifique des erreurs à chaque étape. Chaque Step se concentre sur la course à pied, tandis que nous consignons et renversons tous les chemins malheureux afin qu'ils soient exposés dans des tests unitaires. Cela prend en charge l'itération sécurisée et les alertes au niveau de la production (en utilisant le plug-in Slack Logger pour Nebula pour tous les journaux WARN et ERROR).

Une remarque concernant la consignation des erreurs: transmettre l'instance de l'étape dans des messages de consignation suppose un niveau de Trust dans ce qui devient visible dans les journaux. La toString() par défaut des classes Apex inclut dans le message toutes les propriétés statiques et au niveau de l'instance. Cela peut être souhaitable — ou cela peut divulguer des informations confidentielles. Bien que la consignation et la sécurité ne soient pas au centre des préoccupations ici, notez que pour certains systèmes, l'adhésion à une interface telle que Step peut également impliquer de forcer un remplacement pour toString().

Une telle méthode impose à chaque créateur d'objet de décider ce qui est autorisé à imprimer, ce qui peut être souhaitable.

Au niveau de la consignation : au niveau StepProcessor, nous utilisons INFO, le niveau sans erreur le plus élevé. À mesure que vous gagnez en précision dans l'application, les niveaux de consignation devraient diminuer en conséquence. Les étapes individuelles peuvent utiliser des DEBUG pour des informations générales, les FINE, FINER et FINEST étant réservés à des produits de plus en plus détaillés. La consignation est autant un art qu'une science, mais suivre ces principes permet de garder les journaux cohérents et utiles.

Avant de passer à autre chose, réfléchissons brièvement à la décision de laisser notre processeur d'étape héberger la logique pour laquelle les étapes sont utilisées. Dans une vaste base de codes, vous pouvez définir des StepProcessor virtuelles ou abstraites, et demander aux sous-classes d'identifier des étapes spécifiques pour établir une séparation appropriée des préoccupations.

Le planificateur finit par invoquer Apex . Une fois le reste de la configuration terminée, la section Apex invocable peut décider quelles étapes doivent être exécutées et transmettre le List<StepType> au processeur :

Il s'agit d'une partie simple de l'équation, qui utilise des enregistrements, des données ou une logique pour déterminer les types d'étape à exécuter. L'action invocable est simple, car nous avons encapsulé la complexité ailleurs. Nous avons également protégé contre les exceptions inattendues et facilité le test de chaque pièce isolée.

Le Kit de développement Apex Slack dépasse le cadre de cet article, mais il convient de revenir sur l ' un des problèmes potentiels liés aux exigences : notifier les personnes de rang supérieur dans la hiérarchie des rôles en fonction d ' un délai configurable. Sur le papier, c'est simple, et vous pourriez (correctement) considérer la System.enqueueJob(this) dans le StepProcessor. Avec System.AsyncOptions, notre penchant initial était d'utiliser la surcharge de enqueueJob pour satisfaire cette exigence.

Pour le moment, cependant, le délai maximal via System.AsyncOptions.MinimumQueueableDelayInMinutes est de 10 minutes. Comme l'exigence est de 120 minutes, il reste quelques options. Une approche naïve pourrait ressembler à ceci :

En pratique, le délai serait transmis dans cette classe, car il est piloté par la configuration.

Nous recommandons cette approche, sauf si vous êtes certain qu'il n'y aura jamais qu'un seul type de notification retardée. Il brûle 11 tâches asynchrones supplémentaires avant de démarrer (ou plus, si le délai augmente). Ce coût peut convenir pour un seul travail, pas pour beaucoup. Vous devez également ajouter une méthode à l'interface de Step afin que chaque étape indique au processeur le temps d'attente avant le redémarrage, ce qui ajoute du bruit.

Cela nous laisse deux possibilités intéressantes :

  • Vous pouvez insérer l'étape retardée dans votre infrastructure de travail existante si vous avez déjà planifié une tâche d'interrogation à un intervalle approprié. Vous devriez également être d'accord avec le délai spécifié frappant jusqu'à 15 minutes plus tard (15 minutes est l'intervalle d'actualisation minimum pour une expression CRON planifiée Apex). Cela correspond à peu près à l'exemple Apex invocable; la planification est effectuée à la place via Apex. En d'autres termes, vous pouvez réutiliser la même architecture basée sur l'Step pour traiter les enregistrements basés sur un horodatage « Commencer après » et choisir les étapes à utiliser en fonction d'un mappage de liste de sélection ou de liste à sélection multiple avec les valeurs d'énumération StepType précédemment affichées.
  • Alternativement, si vous êtes à l’aise pour définir une classe Apex externe supplémentaire, rabattez-vous sur Apex par lot (contrairement à Apex Queueable, qui prend en charge les classes internes, les classes Apex par lot doivent être des classes externes) en utilisant System.scheduleBatch().

Prenons l'exemple Apex par lot. Bien que nous recommandons généralement Apex Queueable pour la flexibilité et le contrôle, c'est un cas où Apex Batch règne toujours en maître:

Imaginez ensuite, dans le StepProcessor, que la méthode de addTypeOneSteps() précédemment montrée soit mise à jour avec cette étape retardée :

Nous recommandons généralement de ne pas sauter autant, mais ce délai d'étape devient un autre bloc de construction réutilisable. Tant que des délais plus longs ne sont pas autorisés dans Apex Queueable, cela représente également la méthode la plus simple pour produire cet effet (sans mécanisme d'interrogation, comme discuté).

Nous avons utilisé la conception orientée objet pour répondre aux exigences et créé un système qui s'adapte tout en équilibrant les coûts à long terme de construction et de maintenance. Bien que la déclaration et l'instanciation d'étapes finissent par l'emporter sur leur place dans le StepProcessor, il n'y a guère de dette technique supplémentaire ici. Avec FlowStep, les administrateurs et les développeurs peuvent décider ensemble quand les solutions sans code ou pro-code sont les plus pertinentes.

En utilisant l'interface System.Finalizer dans l'infrastructure Queueable d'Apex, avec Nebula Logger, nous avons construit un système robuste et testable qui nous alerte des échecs imprévus même si les étapes futures n'ont pas de consignation explicite. Pour nous, ce système réduit heureusement les chiffres et réduit les coûts et la complexité. Il nous a également fourni des connaissances précieuses sur le comportement des curseurs Apex sous de réelles charges de travail, nous aidant à affiner notre approche tout en améliorant la fonctionnalité elle-même.

En décomposant les charges de travail complexes et à haut volume en étapes d'exécution modulaires, l'infrastructure de traitement asynchrone basé sur des étapes transforme les contraintes de la plate-forme en avantages techniques, ce qui permet des performances prévisibles, l'observabilité et la gouvernance à l'échelle de l'entreprise. Les étapes peuvent être configurées par les administrateurs et les développeurs, et dans les deux cas, les auteurs d'étapes peuvent se concentrer en toute sécurité sur le respect des limites du gouverneur de plate-forme de base (par exemple les lignes DML et les lignes de requête récupérées) sans se soucier de la mise à l'échelle de chaque étape.

Pour rendre opérationnel et adopter ce schéma dans toutes les implémentations d'entreprise, les architectes doivent :

  • Évaluez les automatisations existantes afin d'identifier les zones dans lesquelles l'orchestration asynchrone peut aider à améliorer les performances et l'observabilité.
  • Décomposez les grands processus en étapes exécutables discrètes et indépendantes avec des objectifs de traitement clairs et des points d'auteur discrets (comme Flow ou Apex).
  • Définissez et regroupez les types d'étape pour accélérer la réutilisation et la normalisation des étapes entre les unités commerciales.
  • Pilote l'approche avec de nouveaux processus ou des automatisations existantes. Vous serez peut-être surpris de constater le nombre de cas de bord que vous trouvez gratuitement dans les étapes, les soins de votre consignation intégrée et l'observabilité !

James Simone est ingénieur logiciel principal chez Salesforce et possède plus d'une décennie d'expérience sur la plate-forme. Il était client de Salesforce – et propriétaire de produits – avant de se lancer dans le développement, et écrit des plongées techniques approfondies sur Salesforce depuis 2019 dans Les Joies d’Apex. Il a déjà publié des articles sur le blog Salesforce Developer et le blog Salesforce Engineering également.