Gestion du cache des jukebox #17

Open
opened 2026-06-12 15:06:09 +02:00 by shuss · 2 comments
Owner

Les modifications récente de la gestion des jukebox à introduit un gros bug (et probablement un autre) :

  • il faut attendre 1 minutes après que le scan soit terminés pour que le cache interne du controleur soit mis à jour.
  • les scan controllé par les cronjob ne mettent plus à jour le cache des packages vynil.

Ce cache doit être mis à jour dès que la status des jukebox est modifiés, indépendamment de la fin du job de scan.

Les modifications récente de la gestion des jukebox à introduit un gros bug (et probablement un autre) : - il faut attendre 1 minutes après que le scan soit terminés pour que le cache interne du controleur soit mis à jour. - les scan controllé par les cronjob ne mettent plus à jour le cache des packages vynil. Ce cache doit être mis à jour dès que la status des jukebox est modifiés, indépendamment de la fin du job de scan.
shuss added the Kind/Bug
Priority
High
2
Reviewed
Confirmed
4
labels 2026-06-12 15:07:09 +02:00

Analyse

Les deux bugs sont confirmés dans le code. Ils ont été introduits par le commit d39ed30 (« fix: réconciliation JukeBox ne bloque plus sur le job de scan »), qui a remplacé l'attente bloquante de fin de job par une garde en tête de réconciliation (operator/src/jukebox.rs).

Cause racine commune

La mise à jour du cache des packages est conditionnée à l'état du Job scan-<name> (terminal + annotation vynil.solidite.fr/last-scan-time), alors que la donnée du cache provient du status du JukeBox, que l'agent patche directement en fin de scan (set_status_updated / set_status_packages_merge). Le signal utilisé n'est pas le bon : c'est le changement de status qui devrait déclencher la mise à jour, et ce signal existe déjà — le controller kube-rs déclenche une réconciliation sur tout changement de l'objet JukeBox, status inclus.

Bug 1 — latence d'1 minute après la fin du scan

Séquence observée :

  1. L'agent patche le status du JukeBox avec les packages → un événement watch déclenche immédiatement une réconciliation.
  2. À ce moment-là, le Job n'est généralement pas encore marqué Complete → la garde retourne requeue(60s) avant toute mise à jour du cache (jukebox.rs:60-66).
  3. Le passage du Job à Complete ne génère aucun événement (le controller ne watch pas les Jobs, pas de .owns()), donc le cache attend le requeue de 60 s.

Bug 2 — les scans cron ne mettent plus à jour le cache

Le CronJob scan-<name> crée des Jobs à noms générés (scan-<name>-<timestamp>), mais la garde n'inspecte que le Job direct scan-<name> (jukebox.rs:52-56). Séquence :

  1. Le job cron termine, l'agent patche le status → réconciliation déclenchée.
  2. La garde regarde l'ancien Job direct scan-<name>, terminal depuis longtemps, dont la completionTime correspond déjà à l'annotation last-scan-timealready_processed = true → cache jamais mis à jour.

Le cache ne se rafraîchit alors qu'au redémarrage de l'opérateur ou lors d'un nouveau scan direct (force-scan / création).

Correction proposée

Décorréler complètement la mise à jour du cache de l'état des Jobs, comme demandé :

  1. Supprimer le mécanisme last-scan-time (annotation + lecture de completionTime).
  2. En tête de reconcile, avant la garde sur le job en cours : comparer self.status.packages (+ spec.pull_secret) avec l'entrée du cache pour ce jukebox, et faire un upsert par jukebox si différent. L'objet reçu par le watch est déjà frais : pas besoin de re-lister tous les JukeBox (évite aussi la course liste/patch), pas de patch d'annotation.
  3. Retirer l'entrée du cache dans cleanup() lors de la suppression d'un JukeBox (aujourd'hui set_package_cache reconstruit tout, un upsert par jukebox nécessite une suppression explicite).
  4. La garde « job en cours → requeue 60s » reste utile pour ne pas toucher au Job, mais ne doit plus court-circuiter la mise à jour du cache.

Couverture systématique : scan direct, scan cron, force-scan et édition manuelle du status, avec une latence quasi nulle (réconciliation déclenchée par le patch de status lui-même).

Si l'analyse te convient, je crée l'issue GitHub miroir et la PR associée.


Analyse rédigée par Claude (assistant IA), pour le compte de @reivaxm.

## Analyse Les deux bugs sont confirmés dans le code. Ils ont été introduits par le commit `d39ed30` (« fix: réconciliation JukeBox ne bloque plus sur le job de scan »), qui a remplacé l'attente bloquante de fin de job par une garde en tête de réconciliation (`operator/src/jukebox.rs`). ### Cause racine commune La mise à jour du cache des packages est conditionnée à l'état du **Job** `scan-<name>` (terminal + annotation `vynil.solidite.fr/last-scan-time`), alors que la donnée du cache provient du **status du JukeBox**, que l'agent patche directement en fin de scan (`set_status_updated` / `set_status_packages_merge`). Le signal utilisé n'est pas le bon : c'est le changement de status qui devrait déclencher la mise à jour, et ce signal existe déjà — le controller kube-rs déclenche une réconciliation sur tout changement de l'objet JukeBox, status inclus. ### Bug 1 — latence d'1 minute après la fin du scan Séquence observée : 1. L'agent patche le status du JukeBox avec les packages → un événement watch déclenche immédiatement une réconciliation. 2. À ce moment-là, le Job n'est généralement pas encore marqué `Complete` → la garde retourne `requeue(60s)` **avant** toute mise à jour du cache (`jukebox.rs:60-66`). 3. Le passage du Job à `Complete` ne génère aucun événement (le controller ne watch pas les Jobs, pas de `.owns()`), donc le cache attend le requeue de 60 s. ### Bug 2 — les scans cron ne mettent plus à jour le cache Le CronJob `scan-<name>` crée des Jobs à noms générés (`scan-<name>-<timestamp>`), mais la garde n'inspecte que le Job direct `scan-<name>` (`jukebox.rs:52-56`). Séquence : 1. Le job cron termine, l'agent patche le status → réconciliation déclenchée. 2. La garde regarde l'ancien Job direct `scan-<name>`, terminal depuis longtemps, dont la `completionTime` correspond déjà à l'annotation `last-scan-time` → `already_processed = true` → cache jamais mis à jour. Le cache ne se rafraîchit alors qu'au redémarrage de l'opérateur ou lors d'un nouveau scan direct (force-scan / création). ### Correction proposée Décorréler complètement la mise à jour du cache de l'état des Jobs, comme demandé : 1. **Supprimer** le mécanisme `last-scan-time` (annotation + lecture de `completionTime`). 2. En tête de `reconcile`, **avant** la garde sur le job en cours : comparer `self.status.packages` (+ `spec.pull_secret`) avec l'entrée du cache pour ce jukebox, et faire un **upsert par jukebox** si différent. L'objet reçu par le watch est déjà frais : pas besoin de re-lister tous les JukeBox (évite aussi la course liste/patch), pas de patch d'annotation. 3. Retirer l'entrée du cache dans `cleanup()` lors de la suppression d'un JukeBox (aujourd'hui `set_package_cache` reconstruit tout, un upsert par jukebox nécessite une suppression explicite). 4. La garde « job en cours → requeue 60s » reste utile pour ne pas toucher au Job, mais ne doit plus court-circuiter la mise à jour du cache. Couverture systématique : scan direct, scan cron, force-scan et édition manuelle du status, avec une latence quasi nulle (réconciliation déclenchée par le patch de status lui-même). Si l'analyse te convient, je crée l'issue GitHub miroir et la PR associée. --- *Analyse rédigée par Claude (assistant IA), pour le compte de @reivaxm.*

Complément — lien de causalité avec #9 et plan de test complet

Lien avec #9 confirmé

Le mécanisme introduit par d39ed30 (garde + fall-through) combiné à 0633523 (force-scan partiel) est aussi la cause du symptôme de #9. Cycle complet :

  1. Annotation force-scan: <cat>/<pkg> posée → l'opérateur supprime le job direct et le recrée avec SCAN_PACKAGE. Le job partiel tourne, protégé par la garde.
  2. Dès que le job partiel est terminal, la réconciliation suivante ré-applique scan.yaml sans filtre (annotation consommée). spec.template étant immuable, l'apply échoue → la branche de secours (jukebox.rs:167-187) supprime le job partiel et lance un scan complet.

Donc : chaque scan partiel est suivi d'un scan complet non demandé (~0-60 s après), le job partiel étant remplacé par un job de même nom sans SCAN_PACKAGE — d'où l'impression que l'agent ne reçoit jamais le filtre. Détail sur #9.

Correction consolidée (les deux issues, sans casser la feature force-scan)

  1. Cache (cette issue) : upsert par jukebox depuis self.status en tête de réconciliation, indépendant des Jobs ; suppression du mécanisme last-scan-time ; retrait de l'entrée dans cleanup().
  2. Gestion du Job (#9) : ne créer/recréer le Job direct que s'il n'existe pas (scan initial) ou si force-scan est présent. Un job terminal sans annotation n'est jamais touché ; les rescans périodiques restent au CronJob.
  3. Agent (durcissement #9) : supprimer le fallback silencieux vers la liste complète quand le filtre ne matche rien dans le status (log_warn + merge vide à la place).

Plan de test (TDD : écrits avant la correction, échouent sur le code actuel)

Opérateur — reconcile()/cleanup() avec client kube mocké (tower_test::mock, comme dans agent/src/boxes/scan.rs) :

Remédiation #17 :

  • cache_updated_from_status_while_job_running — status à jour, job non terminal → cache mis à jour malgré le requeue (échoue aujourd'hui : early-return avant le cache).
  • cache_updated_on_cron_scan_status_change — job direct terminal avec completionTime == annotation (déjà traité), status modifié par un job cron → cache mis à jour (échoue aujourd'hui : already_processed).
  • cache_upsert_preserves_other_jukeboxes — l'upsert du jukebox A ne supprime pas l'entrée du jukebox B.
  • cache_idempotent_when_status_unchanged — pas d'écriture si le status n'a pas changé.
  • cleanup_removes_cache_entry — suppression du JukeBox → entrée retirée du cache.

Remédiation #9 (côté opérateur) :

  • terminal_partial_job_not_replaced_by_full_scan — job terminal portant SCAN_PACKAGE, pas d'annotation → aucun delete/patch/create du Job (échoue aujourd'hui : delete + recréation full scan).
  • running_job_never_touched — job en cours, pas d'annotation → aucun appel d'écriture sur le Job.

Non-régression des features existantes :

  • initial_scan_job_created_when_absent — pas de Job existant → création (scan initial, comportement d'origine).
  • force_scan_partial_creates_job_with_scan_package — annotation apps/monappli → delete + create avec SCAN_PACKAGE=apps/monappli, annotation retirée après succès (0633523).
  • force_scan_true_creates_full_scan_job — valeur true → job sans SCAN_PACKAGE (0633523).
  • force_scan_annotation_kept_on_job_creation_failure — échec de création → annotation conservée pour retry (d78cba0).
  • reconcile_returns_quickly_on_running_job — job en cours → requeue rapide, pas d'attente bloquante (d39ed30).
  • cronjob_always_applied — le CronJob est maintenu dans tous les chemins.

Agent — scan.rhai (harnais de test rhai existant) :

  • partial_filter_limits_to_matched_repos — filtre présent dans le status → seuls les couples registry/image correspondants sont interrogés.
  • partial_filter_no_match_does_not_full_scan — filtre sans correspondance → aucun appel registre, warning, status inchangé (échoue aujourd'hui : fallback else { list }).
  • set_status_packages_merge_preserves_other_packages — le merge partiel ne perd pas les packages hors filtre.

Si ça te convient, je crée l'issue GitHub miroir (référençant #9 et #17) et la PR avec tests-d'abord.


Analyse rédigée par Claude (assistant IA), pour le compte de @reivaxm.

## Complément — lien de causalité avec #9 et plan de test complet ### Lien avec #9 confirmé Le mécanisme introduit par `d39ed30` (garde + fall-through) combiné à `0633523` (force-scan partiel) est aussi la cause du symptôme de #9. Cycle complet : 1. Annotation `force-scan: <cat>/<pkg>` posée → l'opérateur supprime le job direct et le recrée **avec** `SCAN_PACKAGE`. Le job partiel tourne, protégé par la garde. 2. Dès que le job partiel est terminal, la réconciliation suivante ré-applique `scan.yaml` **sans** filtre (annotation consommée). `spec.template` étant immuable, l'apply échoue → la branche de secours (`jukebox.rs:167-187`) **supprime le job partiel et lance un scan complet**. Donc : chaque scan partiel est suivi d'un scan complet non demandé (~0-60 s après), le job partiel étant remplacé par un job de même nom sans `SCAN_PACKAGE` — d'où l'impression que l'agent ne reçoit jamais le filtre. Détail sur #9. ### Correction consolidée (les deux issues, sans casser la feature force-scan) 1. **Cache** (cette issue) : upsert par jukebox depuis `self.status` en tête de réconciliation, indépendant des Jobs ; suppression du mécanisme `last-scan-time` ; retrait de l'entrée dans `cleanup()`. 2. **Gestion du Job** (#9) : ne créer/recréer le Job direct que s'il n'existe pas (scan initial) ou si `force-scan` est présent. Un job terminal sans annotation n'est jamais touché ; les rescans périodiques restent au CronJob. 3. **Agent** (durcissement #9) : supprimer le fallback silencieux vers la liste complète quand le filtre ne matche rien dans le status (`log_warn` + merge vide à la place). ### Plan de test (TDD : écrits avant la correction, échouent sur le code actuel) **Opérateur — `reconcile()`/`cleanup()` avec client kube mocké (`tower_test::mock`, comme dans `agent/src/boxes/scan.rs`) :** *Remédiation #17 :* - `cache_updated_from_status_while_job_running` — status à jour, job non terminal → cache mis à jour malgré le requeue (échoue aujourd'hui : early-return avant le cache). - `cache_updated_on_cron_scan_status_change` — job direct terminal avec `completionTime` == annotation (déjà traité), status modifié par un job cron → cache mis à jour (échoue aujourd'hui : `already_processed`). - `cache_upsert_preserves_other_jukeboxes` — l'upsert du jukebox A ne supprime pas l'entrée du jukebox B. - `cache_idempotent_when_status_unchanged` — pas d'écriture si le status n'a pas changé. - `cleanup_removes_cache_entry` — suppression du JukeBox → entrée retirée du cache. *Remédiation #9 (côté opérateur) :* - `terminal_partial_job_not_replaced_by_full_scan` — job terminal portant `SCAN_PACKAGE`, pas d'annotation → **aucun** delete/patch/create du Job (échoue aujourd'hui : delete + recréation full scan). - `running_job_never_touched` — job en cours, pas d'annotation → aucun appel d'écriture sur le Job. *Non-régression des features existantes :* - `initial_scan_job_created_when_absent` — pas de Job existant → création (scan initial, comportement d'origine). - `force_scan_partial_creates_job_with_scan_package` — annotation `apps/monappli` → delete + create avec `SCAN_PACKAGE=apps/monappli`, annotation retirée après succès (0633523). - `force_scan_true_creates_full_scan_job` — valeur `true` → job sans `SCAN_PACKAGE` (0633523). - `force_scan_annotation_kept_on_job_creation_failure` — échec de création → annotation conservée pour retry (d78cba0). - `reconcile_returns_quickly_on_running_job` — job en cours → requeue rapide, pas d'attente bloquante (d39ed30). - `cronjob_always_applied` — le CronJob est maintenu dans tous les chemins. **Agent — `scan.rhai` (harnais de test rhai existant) :** - `partial_filter_limits_to_matched_repos` — filtre présent dans le status → seuls les couples registry/image correspondants sont interrogés. - `partial_filter_no_match_does_not_full_scan` — filtre sans correspondance → aucun appel registre, warning, status inchangé (échoue aujourd'hui : fallback `else { list }`). - `set_status_packages_merge_preserves_other_packages` — le merge partiel ne perd pas les packages hors filtre. Si ça te convient, je crée l'issue GitHub miroir (référençant #9 et #17) et la PR avec tests-d'abord. --- *Analyse rédigée par Claude (assistant IA), pour le compte de @reivaxm.*
Sign in to join this conversation.