Blueprint Job¶
Le blueprint job initialise une unité de travail identifiable dont l'exécution
peut être suivie et contrôlée. Il fournit une requête et un résultat typés, les
opérations submit, get_status, cancel, retry, get_result, un handler
métier à compléter et des tests avec un fake déterministe.
Cette capacité est développée dans l'issue #224. Elle est disponible à partir d’Arclith 0.32.0 et d’arclith-cli 0.29.0, publiés selon le processus de release.
Choisir Ce Modèle¶
| Besoin | Modèle adapté |
|---|---|
| Réponse immédiate sans état d'exécution à conserver | Use case synchrone/asynchrone directement attendu |
| Travail soumis, identifiable, à suivre ou annuler | Job |
| Traitement d'une collection de données | Batch, éventuellement exécuté comme job |
| Plusieurs étapes avec checkpoints et reprise | Workflow, hors de cette V1 |
| Cycle de vie métier d'une facture | State-machine |
Un job n'est ni un broker ni une entité métier. job s'ajoute avec
add-blueprint, pas avec add-entity --profile job. Ses opérations ne créent
aucun endpoint FastAPI, tool FastMCP, binding RabbitMQ ni node LangGraph.
Générer Une Feature Sans Entité¶
Installer la CLI publique, puis générer un projet dans un répertoire de travail :
Le projet généré déclare arclith>=0.32.0 et ses dépendances de test.
uv sync installe les paquets publics dans son propre environnement.
--entity et --no-entity sont mutuellement exclusifs ; l'un des deux est
obligatoire. --feature est obligatoire avec --no-entity. Les noms request et
result doivent être des noms de classes Python publics distincts, commençant
par une majuscule. Les imports arbitraires et paramètres inconnus sont refusés.
cancellable doit être un booléen ; max_attempts un entier de 1 à 100 et
retention_days un entier de 1 à 3650. Le fichier YAML déclare les six champs,
y compris version: 1. Au niveau Python, max_attempts=1 est le défaut.
Lier Le Job À Une Entité¶
| Bash | |
|---|---|
Le modèle Document doit exister. La requête générée porte entity_id, un UUID
validé puis normalisé en chaîne JSON. Le job possède son propre JobId et son
propre statut. Il ne modifie ni le schéma ni l'état du document. Le handler
reste responsable de l'existence du document, des droits et des effets métier.
La cible peut également référencer un ImmutableRecord existant sans le modifier.
Fichiers Et Composition¶
Les fichiers suivent le layout src/<package>/ :
Les exceptions sont réutilisées depuis arclith.domain.errors.job, sans créer
cinq variantes métier identiques. Le container reçoit un JobRunnerPort et le
JobStorePort utilisé par ce runner ; les requêtes de statut/résultat doivent
lire le même store. La logique applicative ne dépend d'aucun adapter concret.
Le handler initial lève NotImplementedError. Les tests fournissent un fake
explicite et vérifient aussi que ce point d'extension ne prétend pas effectuer
un travail métier. Après ajout de champs obligatoires, enrichir leurs fixtures
avec des exemples représentatifs du projet.
Exécuter Et Observer En Python¶
Cet exemple complet utilise directement les primitives du framework :
submit enregistre un job queued ; il ne crée aucune tâche en arrière-plan.
await runner.run(job_id) réclame atomiquement puis exécute une tentative.
Si l'application place cet appel dans une tâche asyncio pour observer/annuler
le job en parallèle, elle doit posséder et attendre cette tâche, y compris lors
de l'arrêt. Deux appels concurrents à run n'exécutent pas deux fois la même
tentative : le second reçoit une erreur de transition ou de version.
Cycle De Vie, Annulation Et Retry¶
| Text Only | |
|---|---|
| Opération | Précondition | Effet |
|---|---|---|
submit |
Requête et politique valides | Nouveau JobId UUIDv7 ou job idempotent existant |
run (adapter mémoire) |
queued |
Démarre une tentative, puis publie son issue |
cancel |
Annulable, queued |
cancelled, sans appel au handler |
cancel |
Annulable, running |
Positionne cancellation_requested |
retry |
failed, attempt < max_attempts |
Même JobId, tentative suivante en queued |
get_result |
succeeded |
Enveloppe JobResult[T] V1 |
Le token se consulte avec await context.cancellation.is_requested() ;
await context.cancellation.checkpoint() lève JobCancellationRequested que
le runner convertit en cancelled. Les checkpoints doivent se trouver à des
points où une interruption est acceptable. Un handler qui termine avant de
reconnaître la demande peut réussir. Un calcul bloquant ne peut pas être tué
de manière sûre par ce mécanisme. Une annulation déjà acquise est idempotente.
Un retry remet à zéro progression, erreur courante, dates de la tentative et
demande d'annulation. Il conserve l'identité, la requête et l'historique minimal
des tentatives terminées. Le résultat n'est disponible qu'après succès. Aucun
retry n'est automatique, même si max_attempts est supérieur à 1.
Chaque mutation effective incrémente version. Le store accepte uniquement les
changements typés de arclith.domain.services.job_lifecycle, avec une version
attendue ; aucun save(record) arbitraire n'est exposé. Horodatages UTC, ordre
temporel, historique et cohérence statut/résultat/erreur sont validés.
Idempotence, Données Et Erreurs¶
L'unicité d'une clé s'applique à une instance de store et une définition de
job. Même clé et même requête canonique renvoient le job existant, quel que
soit son statut, sans le relancer. Changer le payload ou la politique avec la
même clé lève JobIdempotencyConflict. Sans clé, chaque soumission crée un job.
Ne pas partager un store entre définitions différentes.
Les valeurs sont copiées à l'entrée et à la sortie. Le calcul canonique lit les champs déclarés, y compris ceux exclus de la sérialisation de présentation ; aliases et serializers Pydantic ne peuvent pas masquer une différence de requête. La politique est incluse dans l'empreinte. Les payloads doivent correspondre exactement aux classes request/result déclarées au store.
Requêtes et résultats acceptent des valeurs JSON natives et modèles Pydantic
imbriqués. Ils sont limités à 64 KiB et 32 niveaux de profondeur ; l'enveloppe
de soumission compte dans cette limite. Les flottants non finis, objets SDK et
objets SecretStr/SecretBytes sont refusés. Sérialiser explicitement dates et
UUID métier en chaînes. Les gros résultats passent par une référence vers du
storage ; les credentials passent par des ports injectés, jamais par le job.
La limite technique ne détecte pas un secret placé dans une chaîne ordinaire :
le projet doit exclure les champs sensibles de ses contrats.
La progression optionnelle utilise JobProgress(percent=…), nombre fini entre
0 et 100, ou JobProgress(completed=…, total=…), avec des compteurs entiers
de 0 à 10¹². total=None indique un total inconnu et n'invente aucun pourcentage ;
un total fourni doit être supérieur ou égal à completed. Au moins un pourcentage
ou un compteur completed est requis. La progression ne contient ni logs ni payload
métier. Le contexte mémoire expose aussi job_id pour rattacher un rapport externe
à l'exécution, comme le fait synchronization.
JobError est versionné
et expose trois codes fixes : handler_failed, handler_not_implemented,
execution_interrupted, avec une propriété message stable. Le texte brut et
la trace d'une exception ne sont jamais copiés dans le record public.
Erreurs applicatives : JobNotFound, JobResultUnavailable,
JobTransitionError, JobVersionConflict, JobIdempotencyConflict.
Instrumenter le handler et son propriétaire avec JobId, correlation ID,
attempt, durée et statut. Réserver les traces techniques au système
d'observabilité et ne pas journaliser les payloads.
Rétention Et Limites Mémoire¶
Le store et le runner mémoire sont non durables et réservés au développement et aux tests. Ils coordonnent les coroutines d'une même boucle asyncio ; ils ne coordonnent ni processus, ni machines, ni boucles distinctes. Un arrêt du processus perd les jobs et leurs clés d'idempotence. Les calculs CPU bloquants bloquent la boucle : utiliser un adapter d'exécution approprié.
La rétention part de la fin de la dernière tentative. Le nettoyage est une
action explicite await store.purge_expired() ; aucun timer/cron n'est créé.
Il retire les jobs terminés ayant atteint l'échéance ainsi que leurs clés. Les
jobs actifs sont conservés. Après purge, une clé peut créer un nouveau job :
c'est la limite de la fenêtre d'idempotence. Un job failed purgé ne peut plus
être retenté par son ancien identifiant.
L'annulation de la tâche propriétaire de run enregistre execution_interrupted
puis propage asyncio.CancelledError, tant que le store reste disponible et
que le processus permet d'effectuer ce nettoyage. Ce n'est pas une garantie de
récupération après arrêt brutal. Les erreurs de persistance doivent être
traitées par le propriétaire, sans supposer que le travail n'a pas été exécuté.
Manifestes, Recettes Et Évolution¶
job écrit un manifeste V3 :
| YAML | |
|---|---|
Pour une entité, target contient kind: entity et entity: {name, module}.
Les formats V1 CRUD/append-only et V2 state-machine sont conservés. La recette
garde son enveloppe V1 et versionne les arguments de cible de job avec
target_version: 1, puis entity ou no_entity: true. Elle embarque les
paramètres canoniques et leurs empreintes ; le fichier spec original n'est
plus nécessaire au replay.
Une recette state-machine enregistrée avec CLI 0.28.0 garde son empreinte et
produit les mêmes fichiers octet pour octet. Une migration limitée à une
empreinte de sources auditée neutralise l'ajout de job au catalogue, sans
désactiver les vérifications de dérive des renderers.
Dry-run sans écriture, collisions avant installation, compensation d'une écriture interrompue et préservation du code personnalisé utilisent le plan commun. Une configuration différente pour une feature déjà installée est refusée. Le replay restaure le scaffold, pas les personnalisations métier : conserver ces dernières dans Git. Voir les recettes CLI.
Ajouter Un Adapter De Production¶
Implémenter JobStorePort[RequestT, ResultT] avec soumission/idempotence
atomique, transitions contrôlées et contrôle de version. Implémenter
JobRunnerPort[RequestT, ResultT] pour le moteur choisi, puis réinjecter les
ports dans le container généré. Les cinq use cases et les modèles métier
restent indépendants du fournisseur.
Avant mise en production, tester coordination, durabilité, propriété des workers, récupération des tentatives interrompues, rétention et sécurité des données. Des adapters RabbitMQ/Celery/Kubernetes/Temporal pourront fournir ces garanties ; aucun n'est fourni par cette V1. Prévoir l'idempotence des effets métier : le contrat mémoire ne promet pas d'exécution distribuée exactly-once.