Les applications d’IA modernes ne sont plus de simples appels d’inférence : ce sont des agents de longue durée qui planifient, agissent, observent et réessaient au fil du temps. Une boucle d’agent IA qui récupère du contexte depuis un vector store, appelle un LLM, écrit des résultats en base de données, attend une validation humaine, puis déclenche des actions en aval peut s’exécuter pendant des minutes, des heures, voire des jours. Sans une couche d’orchestration durable, toute défaillance d’infrastructure transitoire relance l’intégralité de la boucle depuis le début : refacturant des appels LLM coûteux, dupliquant les effets de bord et perdant tout le contexte accumulé.
Coordonner ces agents de manière fiable nécessite une couche qui :
- Survit aux pannes : l’état de l’agent est persisté après chaque étape, pas gardé en mémoire
- Garantit l’exécution exactement-une-fois : un appel LLM ou une écriture API externe n’est jamais ré-invoqué après son succès
- Passe à l’échelle horizontalement : des milliers d’instances d’agents concurrentes sans goulots d’étranglement
- Permet des longues attentes : un agent peut attendre des jours une réponse humaine-dans-la-boucle sans maintenir un processus ouvert
Temporal est la plateforme open-source de référence pour ce type de problème. CockroachDB est son backend de persistance distribué idéal, offrant au cluster d’exécution sans état de Temporal un socle de stockage indestructible et répliqué globalement.
Un framework d’orchestration de workflows gère le cycle de vie de programmes multi-étapes à longue durée d’exécution. Au lieu d’écrire des boucles de retry, une logique de point de contrôle et une reprise sur panne manuellement, vous déclarez votre logique métier comme une séquence d’étapes durables et laissez le framework s’occuper du reste. Les promesses fondamentales sont :
- Durabilité : l’état du workflow survit aux pannes de processus, aux redémarrages et aux défaillances d’infrastructure
- Sémantique exactement-une-fois : les étapes individuelles ne sont jamais ré-exécutées après leur complétion
- Idempotence : relancer le même workflow avec le même identifiant est sans effet
- Observabilité : l’historique complet d’exécution est consultable à tout moment
Qu’est-ce que Temporal ?
Temporal est une plateforme open-source, indépendante du langage, pour construire des applications distribuées fiables. Elle introduit le concept d’exécution durable, à savoir la garantie que la logique d’un workflow s’exécute jusqu’à complétion quelle que soit la défaillance d’infrastructure.
Concepts clés
| Concept | Définition |
|---|---|
| Workflow | Fonction tolérante aux pannes orchestrant des Activities ; peut s’exécuter pendant des années |
| Activity | Unité de travail individuelle et retriable (appel API, écriture en base, inférence ML) |
| Worker | Processus qui interroge Temporal pour des tâches et exécute les Workflows et Activities |
| Event History | Journal append-only de chaque Command et Event dans la vie d’un workflow ; source de vérité pour la reprise |
| Namespace | Frontière d’isolation logique ; historiques d’événements, files de tâches et quotas séparés |
| Task Queue | Canal durable reliant un Workflow/Activity à un ensemble de Workers |
| Signal / Query | Mécanismes permettant à du code externe d’envoyer des données à, ou de lire l’état d’un workflow en cours |
Comment fonctionne Temporal
Comprendre pourquoi CockroachDB est le bon backend de persistance pour Temporal nécessite de comprendre comment Temporal stocke son état. Les choix de conception qui rendent Temporal fiable exigent exactement le type de base de données distribuée et fortement cohérente que CockroachDB fournit.
Workflow comme machine à états
Chaque workflow en cours d’exécution est modélisé comme une machine à états. Chaque interaction externe, qu’il s’agisse d’une activité terminée, d’un timer déclenché ou d’un signal reçu, produit un nouvel événement ajouté au journal d’historique du workflow. L’état courant d’un workflow est entièrement déterminé en rejouant ce journal depuis le début.

Une exécution de workflow est une machine à états déterministe pilotée par un historique d’événements en ajout seul
Quand un Worker redémarre après un crash, il récupère l’historique des événements et rejoue la fonction de workflow. Les étapes déjà terminées sont ignorées instantanément ; l’exécution reprend depuis le dernier état validé.
La cohérence est non négociable
Chaque transition d’état doit atomiquement mettre à jour l’état du workflow et mettre en file la prochaine tâche. Si l’une des deux écritures échoue, le système entre dans un état incohérent irrécupérable : une tâche fantôme qui ne sera jamais délivrée.

Les transitions d’état requièrent une mise à jour atomique de l’état du workflow et des entrées de file de tâches dans une seule transaction
C’est pourquoi Temporal exige un store relationnel fortement cohérent avec des garanties ACID complètes, et pourquoi CockroachDB, qui offre l’isolation sérialisable à n’importe quelle échelle, est un choix naturel là où un primaire PostgreSQL unique deviendrait un goulot d’étranglement.
Visibilité : index interrogeable des workflows
En plus du store principal, Temporal maintient un Visibility store, une base de données secondaire optimisée pour interroger les exécutions de workflow par statut, type, heure de démarrage et attributs de recherche personnalisés stockés en JSONB.

Le Visibility store indexe les exécutions de workflow pour les requêtes de liste et filtre via des attributs de recherche JSONB
Le schéma PostgreSQL standard introduit plusieurs incompatibilités avec CockroachDB dès la migration v1.2 (advanced_visibility.sql). Contourner temporal-sql-tool pour la base de visibilité et appliquer directement un schéma compatible CockroachDB résout l’ensemble de ces problèmes (voir l’étape 3 ci-dessous).
Architecture complète du cluster avec CockroachDB
Un cluster Temporal est composé de quatre services sans état passant à l’échelle indépendamment, devant deux niveaux de stockage durable. Quand CockroachDB supporte les deux stores, l’ensemble du niveau de persistance bénéficie de la réplication distribuée, du basculement automatique et de la scalabilité horizontale, de manière transparente pour les services Temporal.

Cluster Temporal avec CockroachDB comme backend de persistance et de visibilité
| Service | Rôle |
|---|---|
| Frontend | Passerelle gRPC : route les requêtes clients vers le bon shard History |
| History | Gère les machines à états des workflows ; traite les commandes et enregistre les événements |
| Matching | Gère les files de tâches ; distribue les tâches aux Workers disponibles |
| Worker | Exécute les workflows système internes (réplication, archivage, nettoyage) |
| Persistence Store (CockroachDB) | Historiques d’événements, timers, files de transfert ; forte cohérence, écritures distribuées |
| Visibility Store (CockroachDB) | Index d’exécution interrogeable ; CREATE INVERTED INDEX remplace les constructions GIN spécifiques à PostgreSQL |
Pourquoi CockroachDB pour Temporal ?
Temporal supporte officiellement PostgreSQL, MySQL, SQLite et Cassandra. CockroachDB convient parfaitement au persistence store car il apporte :
- SQL distribué fortement cohérent : les mêmes garanties ACID que PostgreSQL, à n’importe quelle échelle
- Multi-région actif-actif : les shards d’historique Temporal se distribuent entre régions sans réplication manuelle
- Basculement automatique : les défaillances de nœuds sont transparentes pour les services Temporal
- Scalabilité horizontale : scalez lectures et écritures sans logique de sharding dans l’application
- Protocole filaire PostgreSQL : le plugin
postgres12de Temporal fonctionne directement
Déployer Temporal sur CockroachDB
Étape 1 : Provisionnement des bases et de l’utilisateur
CREATE DATABASE temporal;
CREATE DATABASE temporal_visibility;
CREATE USER temporal;
GRANT ALL ON DATABASE temporal TO temporal;
GRANT ALL ON DATABASE temporal_visibility TO temporal;
En mode non sécurisé (
--insecure/sslmode=disable), CockroachDB n’autorise pas la définition d’un mot de passe. UtilisezCREATE USER temporal;sans clause de mot de passe. Pour un cluster sécurisé, ajoutezWITH PASSWORD 'votre-mot-de-passe'et renseignez le champpassworddans la configuration du serveur.
Étape 2 : Initialisation du schéma de persistance
Le schéma principal fonctionne avec CockroachDB sans modification via l’outil SQL de Temporal. Téléchargez temporal-sql-tool depuis les releases GitHub de Temporal avec temporal-server. Les fichiers de schéma se trouvent dans l’archive source sous schema/postgresql/v12/temporal/versioned/.
Important : passez le nom d’hôte et le port en flags séparés. Le format combiné
--ep host:portest rejeté avec une erreur de détection du port MySQL.
temporal-sql-tool \
--plugin postgres12 \
--ep "<crdb-host>" \
--port 26257 \
--db temporal \
--user temporal \
--tls \
--tls-ca-file /certs/ca.crt \
--tls-cert-file /certs/client.temporal.crt \
--tls-key-file /certs/client.temporal.key \
setup-schema -v 0.0
temporal-sql-tool \
--plugin postgres12 \
--ep "<crdb-host>" \
--port 26257 \
--db temporal \
--user temporal \
--tls \
--tls-ca-file /certs/ca.crt \
--tls-cert-file /certs/client.temporal.crt \
--tls-key-file /certs/client.temporal.key \
update-schema -d ./schema/postgresql/v12/temporal/versioned
Étape 3 : Correction du schéma de visibilité pour CockroachDB
Cette étape nécessite de contourner complètement
temporal-sql-toolpour la base de visibilité. La migrationv1.2(advanced_visibility.sql) introduit quatre incompatibilités avec CockroachDB qui font échouer l’outil. Appliquer directement un schéma adapté viapsqlest la bonne approche.
btree_gin reste bien l’une des incompatibilités : CockroachDB ne supporte pas cette extension. Mais la documentation originale pointait vers la migration v1.1 comme source du problème et traitait btree_gin comme le seul obstacle. Ces deux affirmations sont incorrectes. L’appel à l’extension se trouve dans un bloc anonyme DO LANGUAGE 'plpgsql' situé dans v1.2. CockroachDB rejette le bloc DO lui-même avant même d’atteindre la ligne de l’extension : le premier message d’erreur n’est donc pas « extension introuvable » mais « blocs de code anonymes non supportés ». Deux incompatibilités supplémentaires attendent dans le même fichier après cela.
Les quatre incompatibilités, toutes présentes dans schema/postgresql/v12/visibility/versioned/v1.2/advanced_visibility.sql :
| Incompatibilité | Cause racine | Correctif |
|---|---|---|
DO LANGUAGE 'plpgsql' $$...$$ |
Les blocs de code anonymes ne sont pas supportés dans CockroachDB | Supprimer entièrement ; aucune configuration d’extension n’est nécessaire |
Type de colonne TSVECTOR |
Non supporté dans CockroachDB | Remplacer par VARCHAR(4096) |
(s::timestamptz AT TIME ZONE 'UTC') dans les colonnes calculées STORED |
Cast dépendant du contexte ; CockroachDB le rejette dans les colonnes calculées stockées | Utiliser parse_timestamp(s) à la place |
USING GIN (namespace_id, col jsonb_path_ops) |
Index GIN multi-colonnes avec jsonb_path_ops non supporté |
Utiliser CREATE INVERTED INDEX (col) sur la seule colonne JSONB |
Le schéma doit également couvrir toutes les migrations jusqu’en v1.13, ce qu’exige la vérification de version au démarrage de Temporal. Sauvegardez le contenu suivant dans crdb_visibility_schema.sql et appliquez-le directement :
-- Tables de versionnement du schéma (requises pour la vérification au démarrage de Temporal)
CREATE TABLE IF NOT EXISTS schema_version (
version_partition INT NOT NULL,
db_name VARCHAR(255) NOT NULL,
creation_time TIMESTAMP,
curr_version VARCHAR(64),
min_compatible_version VARCHAR(64),
PRIMARY KEY (version_partition, db_name)
);
CREATE TABLE IF NOT EXISTS schema_update_history (
version_partition INT NOT NULL,
year INT NOT NULL,
month INT NOT NULL,
update_time TIMESTAMP,
description VARCHAR(255),
manifest_md5 VARCHAR(64),
new_version VARCHAR(64),
old_version VARCHAR(64),
PRIMARY KEY (version_partition, year, month, update_time)
);
-- executions_visibility avec toutes les colonnes jusqu'à v1.13
-- TSVECTOR -> VARCHAR ; parse_timestamp() remplace ::timestamp ; btree_gin inutile
CREATE TABLE executions_visibility (
namespace_id CHAR(64) NOT NULL,
run_id CHAR(64) NOT NULL,
start_time TIMESTAMP NOT NULL,
execution_time TIMESTAMP NOT NULL,
workflow_id VARCHAR(255) NOT NULL,
workflow_type_name VARCHAR(255) NOT NULL,
status INTEGER NOT NULL,
close_time TIMESTAMP NULL,
history_length BIGINT,
history_size_bytes BIGINT NULL,
execution_duration BIGINT NULL,
state_transition_count BIGINT NULL,
memo BYTEA,
encoding VARCHAR(64) NOT NULL,
task_queue VARCHAR(255) DEFAULT '' NOT NULL,
search_attributes JSONB NULL,
parent_workflow_id VARCHAR(255) NULL,
parent_run_id VARCHAR(255) NULL,
root_workflow_id VARCHAR(255) NOT NULL DEFAULT '',
root_run_id VARCHAR(255) NOT NULL DEFAULT '',
_version BIGINT NOT NULL DEFAULT 0,
-- Attributs de recherche prédéfinis (calculés depuis le blob JSONB search_attributes)
TemporalChangeVersion JSONB AS (search_attributes->'TemporalChangeVersion') STORED,
BinaryChecksums JSONB AS (search_attributes->'BinaryChecksums') STORED,
BuildIds JSONB AS (search_attributes->'BuildIds') STORED,
BatcherUser VARCHAR(255) AS (search_attributes->>'BatcherUser') STORED,
TemporalScheduledStartTime TIMESTAMP AS (parse_timestamp(search_attributes->>'TemporalScheduledStartTime')) STORED,
TemporalScheduledById VARCHAR(255) AS (search_attributes->>'TemporalScheduledById') STORED,
TemporalSchedulePaused BOOLEAN AS ((search_attributes->'TemporalSchedulePaused')::boolean) STORED,
TemporalNamespaceDivision VARCHAR(255) AS (search_attributes->>'TemporalNamespaceDivision') STORED,
TemporalPauseInfo JSONB AS (search_attributes->'TemporalPauseInfo') STORED,
TemporalWorkerDeploymentVersion VARCHAR(255) AS (search_attributes->>'TemporalWorkerDeploymentVersion') STORED,
TemporalWorkflowVersioningBehavior VARCHAR(255) AS (search_attributes->>'TemporalWorkflowVersioningBehavior') STORED,
TemporalWorkerDeployment VARCHAR(255) AS (search_attributes->>'TemporalWorkerDeployment') STORED,
TemporalReportedProblems JSONB AS (search_attributes->'TemporalReportedProblems') STORED,
TemporalBool01 BOOLEAN AS ((search_attributes->'TemporalBool01')::boolean) STORED,
TemporalBool02 BOOLEAN AS ((search_attributes->'TemporalBool02')::boolean) STORED,
TemporalDatetime01 TIMESTAMP AS (parse_timestamp(search_attributes->>'TemporalDatetime01')) STORED,
TemporalDatetime02 TIMESTAMP AS (parse_timestamp(search_attributes->>'TemporalDatetime02')) STORED,
TemporalDouble01 DECIMAL(20,5) AS ((search_attributes->'TemporalDouble01')::decimal) STORED,
TemporalDouble02 DECIMAL(20,5) AS ((search_attributes->'TemporalDouble02')::decimal) STORED,
TemporalInt01 BIGINT AS ((search_attributes->'TemporalInt01')::bigint) STORED,
TemporalInt02 BIGINT AS ((search_attributes->'TemporalInt02')::bigint) STORED,
TemporalKeyword01 VARCHAR(255) AS (search_attributes->>'TemporalKeyword01') STORED,
TemporalKeyword02 VARCHAR(255) AS (search_attributes->>'TemporalKeyword02') STORED,
TemporalKeyword03 VARCHAR(255) AS (search_attributes->>'TemporalKeyword03') STORED,
TemporalKeyword04 VARCHAR(255) AS (search_attributes->>'TemporalKeyword04') STORED,
TemporalKeywordList01 JSONB AS (search_attributes->'TemporalKeywordList01') STORED,
TemporalKeywordList02 JSONB AS (search_attributes->'TemporalKeywordList02') STORED,
TemporalLowCardinalityKeyword01 VARCHAR(255) AS (search_attributes->>'TemporalLowCardinalityKeyword01') STORED,
TemporalUsedWorkerDeploymentVersions JSONB AS (search_attributes->'TemporalUsedWorkerDeploymentVersions') STORED,
-- Attributs de recherche personnalisés pré-alloués
Bool01 BOOLEAN AS ((search_attributes->'Bool01')::boolean) STORED,
Bool02 BOOLEAN AS ((search_attributes->'Bool02')::boolean) STORED,
Bool03 BOOLEAN AS ((search_attributes->'Bool03')::boolean) STORED,
Datetime01 TIMESTAMP AS (parse_timestamp(search_attributes->>'Datetime01')) STORED,
Datetime02 TIMESTAMP AS (parse_timestamp(search_attributes->>'Datetime02')) STORED,
Datetime03 TIMESTAMP AS (parse_timestamp(search_attributes->>'Datetime03')) STORED,
Double01 DECIMAL(20,5) AS ((search_attributes->'Double01')::decimal) STORED,
Double02 DECIMAL(20,5) AS ((search_attributes->'Double02')::decimal) STORED,
Double03 DECIMAL(20,5) AS ((search_attributes->'Double03')::decimal) STORED,
Int01 BIGINT AS ((search_attributes->'Int01')::bigint) STORED,
Int02 BIGINT AS ((search_attributes->'Int02')::bigint) STORED,
Int03 BIGINT AS ((search_attributes->'Int03')::bigint) STORED,
Keyword01 VARCHAR(255) AS (search_attributes->>'Keyword01') STORED,
Keyword02 VARCHAR(255) AS (search_attributes->>'Keyword02') STORED,
Keyword03 VARCHAR(255) AS (search_attributes->>'Keyword03') STORED,
Keyword04 VARCHAR(255) AS (search_attributes->>'Keyword04') STORED,
Keyword05 VARCHAR(255) AS (search_attributes->>'Keyword05') STORED,
Keyword06 VARCHAR(255) AS (search_attributes->>'Keyword06') STORED,
Keyword07 VARCHAR(255) AS (search_attributes->>'Keyword07') STORED,
Keyword08 VARCHAR(255) AS (search_attributes->>'Keyword08') STORED,
Keyword09 VARCHAR(255) AS (search_attributes->>'Keyword09') STORED,
Keyword10 VARCHAR(255) AS (search_attributes->>'Keyword10') STORED,
Text01 VARCHAR(4096) AS (search_attributes->>'Text01') STORED,
Text02 VARCHAR(4096) AS (search_attributes->>'Text02') STORED,
Text03 VARCHAR(4096) AS (search_attributes->>'Text03') STORED,
KeywordList01 JSONB AS (search_attributes->'KeywordList01') STORED,
KeywordList02 JSONB AS (search_attributes->'KeywordList02') STORED,
KeywordList03 JSONB AS (search_attributes->'KeywordList03') STORED,
PRIMARY KEY (namespace_id, run_id)
);
-- Index d'expression standards (fenêtre ouverte/fermée avec COALESCE)
CREATE INDEX default_idx ON executions_visibility (namespace_id, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_execution_time ON executions_visibility (namespace_id, execution_time, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_workflow_id ON executions_visibility (namespace_id, workflow_id, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_workflow_type ON executions_visibility (namespace_id, workflow_type_name, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_status ON executions_visibility (namespace_id, status, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_history_length ON executions_visibility (namespace_id, history_length, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_history_size_bytes ON executions_visibility (namespace_id, history_size_bytes, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_execution_duration ON executions_visibility (namespace_id, execution_duration, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_state_transition_count ON executions_visibility (namespace_id, state_transition_count, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_task_queue ON executions_visibility (namespace_id, task_queue, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_parent_workflow_id ON executions_visibility (namespace_id, parent_workflow_id, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_parent_run_id ON executions_visibility (namespace_id, parent_run_id, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_root_workflow_id ON executions_visibility (namespace_id, root_workflow_id, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_root_run_id ON executions_visibility (namespace_id, root_run_id, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_batcher_user ON executions_visibility (namespace_id, BatcherUser, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_scheduled_start_time ON executions_visibility (namespace_id, TemporalScheduledStartTime, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_scheduled_by_id ON executions_visibility (namespace_id, TemporalScheduledById, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_schedule_paused ON executions_visibility (namespace_id, TemporalSchedulePaused, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_namespace_division ON executions_visibility (namespace_id, TemporalNamespaceDivision, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_worker_deployment_version ON executions_visibility (namespace_id, TemporalWorkerDeploymentVersion, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_workflow_versioning_behavior ON executions_visibility (namespace_id, TemporalWorkflowVersioningBehavior, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_worker_deployment ON executions_visibility (namespace_id, TemporalWorkerDeployment, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_bool_01 ON executions_visibility (namespace_id, TemporalBool01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_bool_02 ON executions_visibility (namespace_id, TemporalBool02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_datetime_01 ON executions_visibility (namespace_id, TemporalDatetime01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_datetime_02 ON executions_visibility (namespace_id, TemporalDatetime02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_double_01 ON executions_visibility (namespace_id, TemporalDouble01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_double_02 ON executions_visibility (namespace_id, TemporalDouble02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_int_01 ON executions_visibility (namespace_id, TemporalInt01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_int_02 ON executions_visibility (namespace_id, TemporalInt02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_keyword_01 ON executions_visibility (namespace_id, TemporalKeyword01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_keyword_02 ON executions_visibility (namespace_id, TemporalKeyword02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_keyword_03 ON executions_visibility (namespace_id, TemporalKeyword03, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_keyword_04 ON executions_visibility (namespace_id, TemporalKeyword04, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_temporal_low_cardinality_keyword_01 ON executions_visibility (namespace_id, TemporalLowCardinalityKeyword01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_bool_01 ON executions_visibility (namespace_id, Bool01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_bool_02 ON executions_visibility (namespace_id, Bool02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_bool_03 ON executions_visibility (namespace_id, Bool03, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_datetime_01 ON executions_visibility (namespace_id, Datetime01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_datetime_02 ON executions_visibility (namespace_id, Datetime02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_datetime_03 ON executions_visibility (namespace_id, Datetime03, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_double_01 ON executions_visibility (namespace_id, Double01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_double_02 ON executions_visibility (namespace_id, Double02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_double_03 ON executions_visibility (namespace_id, Double03, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_int_01 ON executions_visibility (namespace_id, Int01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_int_02 ON executions_visibility (namespace_id, Int02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_int_03 ON executions_visibility (namespace_id, Int03, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_01 ON executions_visibility (namespace_id, Keyword01, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_02 ON executions_visibility (namespace_id, Keyword02, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_03 ON executions_visibility (namespace_id, Keyword03, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_04 ON executions_visibility (namespace_id, Keyword04, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_05 ON executions_visibility (namespace_id, Keyword05, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_06 ON executions_visibility (namespace_id, Keyword06, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_07 ON executions_visibility (namespace_id, Keyword07, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_08 ON executions_visibility (namespace_id, Keyword08, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_09 ON executions_visibility (namespace_id, Keyword09, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
CREATE INDEX by_keyword_10 ON executions_visibility (namespace_id, Keyword10, (COALESCE(close_time, '9999-12-31 23:59:59')) DESC, start_time DESC, run_id);
-- Index inversés CockroachDB remplaçant GIN multi-colonnes (namespace_id, col jsonb_path_ops)
CREATE INVERTED INDEX by_temporal_change_version ON executions_visibility (TemporalChangeVersion);
CREATE INVERTED INDEX by_binary_checksums ON executions_visibility (BinaryChecksums);
CREATE INVERTED INDEX by_build_ids ON executions_visibility (BuildIds);
CREATE INVERTED INDEX by_temporal_pause_info ON executions_visibility (TemporalPauseInfo);
CREATE INVERTED INDEX by_temporal_reported_problems ON executions_visibility (TemporalReportedProblems);
CREATE INVERTED INDEX by_temporal_keyword_list_01 ON executions_visibility (TemporalKeywordList01);
CREATE INVERTED INDEX by_temporal_keyword_list_02 ON executions_visibility (TemporalKeywordList02);
CREATE INVERTED INDEX by_keyword_list_01 ON executions_visibility (KeywordList01);
CREATE INVERTED INDEX by_keyword_list_02 ON executions_visibility (KeywordList02);
CREATE INVERTED INDEX by_keyword_list_03 ON executions_visibility (KeywordList03);
CREATE INVERTED INDEX by_used_deployment_versions ON executions_visibility (TemporalUsedWorkerDeploymentVersions);
-- Définit la version du schéma pour que la vérification au démarrage de Temporal passe
INSERT INTO schema_version (version_partition, db_name, creation_time, curr_version, min_compatible_version)
VALUES (0, 'temporal_visibility', now(), '1.13', '0.1')
ON CONFLICT DO NOTHING;
Appliquez-le avec psql (le CLI cockroach n’est pas nécessaire) :
psql "postgresql://temporal@<crdb-host>:26257/temporal_visibility?sslmode=disable" \
--file ./crdb_visibility_schema.sql
Étape 4 : Configurer et démarrer le serveur Temporal
Sauvegardez le contenu suivant dans base.yaml. La configuration doit être dans un fichier ; le flag --config-file accepte un chemin absolu ou relatif au répertoire courant :
log:
stdout: true
level: "info"
persistence:
defaultStore: crdb-default
visibilityStore: crdb-visibility
numHistoryShards: 4
datastores:
crdb-default:
sql:
pluginName: "postgres12"
databaseName: "temporal"
connectAddr: "<crdb-host>:26257"
connectProtocol: "tcp"
user: "temporal"
password: "${TEMPORAL_DB_PASSWORD}"
maxConns: 20
maxIdleConns: 20
maxConnLifetime: "1h"
tls:
enabled: true
caFile: "/certs/ca.crt"
certFile: "/certs/client.temporal.crt"
keyFile: "/certs/client.temporal.key"
serverName: "<crdb-host>"
crdb-visibility:
sql:
pluginName: "postgres12"
databaseName: "temporal_visibility"
connectAddr: "<crdb-host>:26257"
connectProtocol: "tcp"
user: "temporal"
password: "${TEMPORAL_DB_PASSWORD}"
maxConns: 10
maxIdleConns: 10
maxConnLifetime: "1h"
tls:
enabled: true
caFile: "/certs/ca.crt"
certFile: "/certs/client.temporal.crt"
keyFile: "/certs/client.temporal.key"
serverName: "<crdb-host>"
global:
membership:
maxJoinDuration: 30s
broadcastAddress: "127.0.0.1"
services:
frontend:
rpc:
grpcPort: 7233
membershipPort: 6933
bindOnLocalHost: true
matching:
rpc:
grpcPort: 7235
membershipPort: 6935
bindOnLocalHost: true
history:
rpc:
grpcPort: 7234
membershipPort: 6934
bindOnLocalHost: true
worker:
rpc:
grpcPort: 7239
membershipPort: 6939
bindOnLocalHost: true
clusterMetadata:
enableGlobalNamespace: false
failoverVersionIncrement: 10
masterClusterName: "active"
currentClusterName: "active"
clusterInformation:
active:
enabled: true
initialFailoverVersion: 1
rpcAddress: "127.0.0.1:7233"
dcRedirectionPolicy:
policy: "noop"
archival:
history:
state: "disabled"
visibility:
state: "disabled"
namespaceDefaults:
archival:
history:
state: "disabled"
visibility:
state: "disabled"
Démarrez le serveur avec --allow-no-auth (requis quand aucun autoriseur n’est configuré) :
temporal-server --config-file ./base.yaml --allow-no-auth start
Étape 5 : Initialisation du cluster
Une fois le serveur démarré, créez le namespace par défaut et vérifiez le cluster :
# Créer le namespace applicatif
temporal --address localhost:7233 operator namespace create default
# Confirmer que le cluster répond
temporal --address localhost:7233 operator cluster health
# Lister les workflows système internes pour confirmer la connexion au visibility store
temporal --address localhost:7233 -n temporal-system workflow list
Le health check doit renvoyer SERVING et la liste doit afficher deux workflows système en cours (temporal-sys-history-scanner et temporal-sys-tq-scanner).
Étape 6 : Premier workflow d’agent IA durable
La boucle d’agent suivante récupère du contexte, appelle un LLM, attend une validation humaine et écrit le résultat final. Chaque étape est une Activity ; elle s’exécute exactement une fois même si le processus crashe entre les étapes. Un appel LLM coûteux n’est jamais ré-émis après son succès.
from temporalio import workflow, activity
from temporalio.common import RetryPolicy
from datetime import timedelta
@activity.defn
async def retrieve_context(task: str) -> str:
"""Query a vector store for relevant context."""
return await vector_store.search(task)
@activity.defn
async def call_llm(context: str) -> str:
"""Call the LLM — billed once, never re-executed on retry."""
return await llm_client.complete(f"Given this context: {context}, respond.")
@activity.defn
async def request_human_approval(response: str) -> bool:
"""Write pending approval to DB — the agent can wait days here."""
return await approvals_db.create_pending(response)
@activity.defn
async def write_final_result(result: str) -> None:
"""Persist the approved result — exactly once."""
await results_db.insert(result)
@workflow.defn
class AICockroachAgentWorkflow:
@workflow.run
async def run(self, task: str) -> str:
# Step 1 — retrieve context (retries safely, idempotent)
context = await workflow.execute_activity(
retrieve_context, task,
start_to_close_timeout=timedelta(minutes=2),
)
# Step 2 — LLM call (exactly once — no double billing)
response = await workflow.execute_activity(
call_llm, context,
start_to_close_timeout=timedelta(minutes=5),
retry_policy=RetryPolicy(maximum_attempts=3),
)
# Step 3 — human-in-the-loop (agent sleeps until approved, days if needed)
approved = await workflow.execute_activity(
request_human_approval, response,
start_to_close_timeout=timedelta(days=7),
)
# Step 4 — persist result (idempotent write, exactly once)
if approved:
await workflow.execute_activity(
write_final_result, response,
start_to_close_timeout=timedelta(seconds=30),
)
return response
Bénéfices clés
| Capacité | Contribution de CockroachDB |
|---|---|
| Isolation sérialisable | Pas de mises à jour perdues ni de lectures fantômes sous exécution concurrente |
| Réplication multi-région | Shards d’historique durables à travers les défaillances de data center |
| Scalabilité horizontale | Ajoutez des nœuds pour absorber plus de workflows concurrents sans re-sharding |
| Basculement automatique | Défaillances de nœuds transparentes pour les quatre services Temporal |
| Compatibilité PostgreSQL | Aucune modification du code applicatif ; le plugin postgres12 fonctionne directement |
CockroachDB remplace PostgreSQL directement, offrant aux services sans état de Temporal une fondation indestructible et distribuée globalement. Le seul travail de déploiement supplémentaire par rapport à une installation PostgreSQL standard est l’application d’un schéma de visibilité adapté à CockroachDB, qui résout quatre constructions PostgreSQL non supportées.