ADR 0005 — Reindex en thread interne plutôt que worker séparé
Date : initial Statut : Accepté
Contexte
Le service doit rafraîchir périodiquement son index (~toutes les 15 min) en pullant l'amont Horizon, calculant les embeddings, et UPSERTant les records. Trois architectures possibles :
- Service worker dédié (Docker container séparé, partage la DB) — modèle Horizon/Worker.
- Scheduler externe (cron Kubernetes, GitLab pipelines, GitHub Actions) qui appelle
GET /reindex. - Thread de fond dans le même process que l'API.
Décision
Option 3 — Thread de fond dans le process FastAPI, démarré au boot par src.schedule_runner.start_schedule(). Lock global threading.Lock pour éviter les exécutions concurrentes. Thread daemon=True pour qu'un SIGTERM du conteneur tue tout proprement.
python
# src/schedule_runner.py
def start_schedule():
def target():
while True:
counter = 0
logger.info("Starting reindexing, waiting %s seconds.", REINDEX_FREQ)
reindex() # GET /reindex sans HTTP
while counter < REINDEX_FREQ:
time.sleep(1)
counter += 1
Thread(target=target).start()python
# src/reindex.py
_reindex_lock = Lock()
_reindex_running = False
def reindex(pool) -> bool:
global _reindex_running
with _reindex_lock:
if _reindex_running:
return False # déjà en cours, no-op
_reindex_running = True
Thread(target=_run_reindex, args=(pool,), daemon=True).start()
return TrueLe thread fait son travail dans _do_reindex(pool), le finally libère _reindex_running même en cas d'exception.
Conséquences
Positives
- ✅ Un seul process à déployer. Pas de container séparé, pas de configuration de réseau supplémentaire entre worker et API.
- ✅ Pas de scheduler externe à provisionner. Pas de dépendance à un cron Kubernetes ou un GitLab pipeline schedule (qui ajouterait une latence d'observabilité).
- ✅ Code simple. ~30 lignes au total entre
schedule_runner.pyet le lock dansreindex.py. - ✅ Concurrence bornée par le lock global. Le déclencheur (boot, scheduler, manual
/reindex) n'a pas d'importance — un seul reindex à la fois. - ✅ Mêmes credentials et même pool DB que l'API, gratuit.
- ✅ Test trivial. Les tests appellent
_do_reindex(pool)synchrone, sans mock du thread.
Négatives
- ❌ Pas de scaling horizontal. Si on déploie 2 répliques du service, chaque réplique fera son propre reindex. Coût Infomaniak ×2, et possible race sur le DELETE final (les
new_idspeuvent diverger si l'amont change entre les deux pulls). À ce stade, le service tourne en mono-réplique → pas un problème. - ❌ Reindex partagé avec l'API. Un reindex consomme du CPU (encoding embeddings, parsing JSON, INSERT). Latence des requêtes API peut monter pendant un cycle. Mitigeable par mono-thread + asyncio si nécessaire (mais aujourd'hui pas observé).
- ❌ Pas d'observabilité fine du thread. Le
Threadestdaemon=True; si letargetlève une exception non capturée, on la voit en log mais pas en métrique._run_reindexcapture les exceptions explicitement pour parer à ça. - ❌ Pas de queue de jobs ni de retry. Un reindex échoué attend simplement le prochain cycle. Acceptable car les données « stale » restent interrogeables ; l'amont n'évolue pas trop vite.
- ❌
time.sleep(1)en boucle dansstart_schedule()(au lieu d'untime.sleep(REINDEX_FREQ)direct) : volontaire pour permettre une interruption rapide si on ajoutait plus tard un signal de shutdown — mais lesignal(SIGTERM, shutdown_handler)actuel faitsys.exit(0)brutal, donc c'est un peu de sur-ingénierie.
Alternatives écartées
- Container worker séparé (modèle Horizon) : sur-ingéniérie pour ce volume. Le worker Horizon est justifié par le nombre et la diversité des jobs (paiements, Visual Planning, mailing…). Ici on a un seul job.
- Cron Kubernetes / GitLab pipeline schedule : ajoute une dépendance externe à provisionner et des secrets HTTP supplémentaires. Et ne résout pas le « reindex au boot si DB stale ».
asyncio+uvicornworkers : changement de paradigme (passage à FastAPI 100% async, pool psycopg async). Bénéfice marginal ici. Reportable si on observe une contention.- Bull / Celery / RQ : surdimensionné pour 1 job toutes les 15 min.
Implications pour les développeurs
- Tests : toujours
BuchardDatabase(pool=…, auto_reindex=False)pour éviter de déclencher un reindex au moment de l'import. Les tests d'ingestion appellent_do_reindex(pool)directement. - Importer
src.dbdéclenchedb = BuchardDatabase()au top-level → potentiel reindex au moment de l'import. Tous les modules qui fontfrom .db import dbhéritent de ce comportement. C'est volontaire pour la prod, contournable pour les tests. - Multi-réplique : si jamais on a besoin de scale horizontalement, il faudra :
- Soit déplacer le reindex vers un container dédié,
- Soit passer le lock en lock distribué (advisory lock Postgres :
pg_try_advisory_lock(<some-id>)).
Contributors
No contributors
Changelog
No recent changes

