Skip to content

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 :

  1. Service worker dédié (Docker container séparé, partage la DB) — modèle Horizon/Worker.
  2. Scheduler externe (cron Kubernetes, GitLab pipelines, GitHub Actions) qui appelle GET /reindex.
  3. 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 True

Le 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.py et le lock dans reindex.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_ids peuvent 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 Thread est daemon=True ; si le target lève une exception non capturée, on la voit en log mais pas en métrique. _run_reindex capture 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 dans start_schedule() (au lieu d'un time.sleep(REINDEX_FREQ) direct) : volontaire pour permettre une interruption rapide si on ajoutait plus tard un signal de shutdown — mais le signal(SIGTERM, shutdown_handler) actuel fait sys.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 + uvicorn workers : 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.db déclenche db = BuchardDatabase() au top-level → potentiel reindex au moment de l'import. Tous les modules qui font from .db import db hé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