P1.5A — Capacity Measurement Contract¶
Status: CURRENT CONTRACT / FOUNDATION / NOT RUNTIME CERTIFIED
Objet¶
P1.5A fixe le vocabulaire de mesure qui sera utilisé pour certifier la capacité réelle du VPS CMonChoix. Ce lot ne déploie aucun exporter supplémentaire, n'ajoute aucun endpoint Runtime, ne modifie aucun scrape Prometheus et n'exécute aucun benchmark live.
L'autorité machine-readable unique est :
monitoring/capacity-metrics.json
Cette page explique cette autorité. En cas d'écart entre une copie documentaire et le registre JSON, le registre JSON doit être corrigé ou la documentation doit être réalignée ; il ne doit jamais exister deux contrats de métriques concurrents.
Audit de départ :
main=b064a04a3d729589171343ab28485310bb8922c3
Principe¶
La stratégie reste :
calculer lorsque les données changent, servir rapidement lorsque les utilisateurs lisent.
Et l'objectif infrastructure reste :
single-node industrial-grade, distributed-ready
P1.5 ne cherche donc pas à prouver un nombre arbitraire de marchands. Il doit mesurer la relation entre :
volume d'offres
× taux de changement
× fréquence de refresh
× coût ingestion / normalisation / persistence / identity / projection
× concurrence
× trafic frontend
Ce qui existe déjà dans le Runtime¶
P1.5A ne part pas d'une page blanche.
Sync runs¶
ccx_sync_runs_v2 porte déjà notamment :
source_feed;status;worker_id;batch_size;total_rows_estimated;processed_rows;success_rows;skipped_rows;error_rows;quarantined_rows;started_at;finished_at;last_heartbeat_at.
Ces données restent la vérité opérationnelle des runs. Prometheus ne doit pas devenir une nouvelle base d'autorité pour leur état.
Sync batches¶
ccx_sync_batches_v2 porte déjà :
rows_seen;rows_ok;rows_skipped;rows_error;elapsed_ms;avg_ms_per_row;peak_memory_mb;current_memory_mb;metrics_json.
Le runner mesure déjà séparément dans metrics_json :
- claim ;
- JSON decode ;
- normalisation ;
- row-to-item ;
- marquage des statuts ;
- persistence.
Ces mesures sont des preuves utiles pour les benchmarks historiques et pour valider la future instrumentation Prometheus.
Queue canonique de traitement¶
ccx_sync_stage_raw_v2 porte :
source_feed;status;created_at;claimed_at;claim_expires_at;processed_at.
Le queue_depth P1.5 désigne les lignes pending + processing des runs reprenables. Les lignes terminales ne sont pas du backlog.
Le queue_lag est l'âge de la plus ancienne ligne encore pending ou processing, calculé depuis created_at. Une queue vide a un lag de zéro.
Catalogue¶
total_offers est dérivé du nombre de lignes de la table canonique ccx_offers_norm_v2.
Aucune table de comptage spécifique P1.5 ne doit être créée pour recopier ce nombre.
Infrastructure Prometheus déjà disponible¶
Sans nouvelle instrumentation, le Runtime dispose déjà via cAdvisor/node-exporter de signaux utilisables pour :
- CPU du conteneur
ccx-platform-worker; - RAM working set du worker ;
- lectures/écritures disque ;
- IOPS host ;
- CPU/RAM host.
P1.5B pourra créer des recording rules stables au-dessus de ces séries sans changer leur source de vérité.
Métriques obligatoires¶
Le registre JSON fixe les sorties de capacité suivantes :
total_offers
offers_changed_per_day
source_rows_per_sec
normalized_rows_per_sec
persisted_rows_per_sec
identity_products_per_sec
projection_products_per_sec
jobs_per_min
DB_writes_per_sec
DB_query_latency
worker_CPU
worker_RAM
disk_IOPS
queue_depth
queue_lag
projection_lag
frontend_p95
frontend_p99
HTTP_5xx
HTTP_5xx est inclus explicitement dans P1.5 car un benchmark de throughput qui dégrade le frontend n'est pas un PASS.
Fenêtre de calcul¶
La fenêtre standard d'observation est :
5 minutes
Les taux et quantiles dashboard/SLO utilisent cette fenêtre sauf justification documentée.
Un benchmark borné peut utiliser son intervalle exact de mesure, à condition :
- de conserver la même sémantique de métrique ;
- d'enregistrer les timestamps début/fin ;
- de ne pas comparer une moyenne de run à un p95 dashboard comme s'il s'agissait du même indicateur.
Sémantique des métriques¶
total_offers¶
Sortie :
ccx_capacity_total_offers
Unité : offres.
Source : COUNT(*) de ccx_offers_norm_v2.
offers_changed_per_day¶
Sortie :
ccx_capacity_offers_changed_24h
Unité : offres modifiées sur 24 h.
La future instrumentation doit compter uniquement les changements sémantiques effectivement commités. Un retry idempotent ne doit pas compter deux fois la même mutation.
Throughput pipeline¶
Les débits sont calculés à partir de compteurs monotones :
ccx_sync_source_rows_total
ccx_sync_normalized_rows_total
ccx_sync_persisted_rows_total
ccx_identity_products_total
ccx_projection_products_total
ccx_jobs_completed_total
Les sorties *_per_second ou *_per_minute sont des dérivations Prometheus, pas des compteurs persistés séparément.
DB writes¶
DB_writes_per_sec est requis mais P1.5A ne prétend pas qu'un exporter MariaDB existe déjà.
P1.5C devra sélectionner et vérifier une source de compteur MariaDB réelle avant de matérialiser :
ccx_capacity_db_writes_per_second
Aucun nom de série exporter non vérifié n'est déclaré certifié dans P1.5A.
DB query latency¶
La mesure primaire de certification est le p95 sur la fenêtre standard :
ccx_capacity_db_query_latency_p95_seconds
La source future est un histogramme borné :
ccx_db_query_duration_seconds
Les labels SQL haute cardinalité sont interdits.
Worker CPU¶
Sortie :
ccx_capacity_worker_cpu_ratio
Le ratio représente le CPU utilisé par ccx-platform-worker rapporté au nombre de cores de la machine.
Worker RAM¶
Sortie :
ccx_capacity_worker_memory_bytes
Source : working set cAdvisor du conteneur ccx-platform-worker.
disk IOPS¶
Sortie :
ccx_capacity_disk_iops
Le calcul additionne les opérations de lecture et d'écriture terminées sur les block devices réels, en excluant les devices loop.
Projection lag¶
Sortie :
ccx_capacity_projection_lag_seconds
Le lag signifie :
timestamp du commit métier exigeant une projection
→ timestamp de fin de la projection affectée
Il est interdit d'approximer ce lag en soustrayant deux MAX(timestamp) globaux sans corrélation entre la mutation et la projection.
Cette métrique nécessite donc une instrumentation corrélée dans un lot ultérieur.
Frontend p95 / p99¶
Source future :
ccx_frontend_request_duration_seconds
Les quantiles sont calculés côté Prometheus depuis l'histogramme. Le code applicatif ne doit pas calculer ou stocker un p95/p99 local.
HTTP 5xx¶
Sortie :
ccx_capacity_http_5xx_ratio
Il s'agit du ratio de requêtes publiques classées 5xx sur le total de requêtes observées sur la même fenêtre.
Cardinalité¶
Les labels dynamiques non bornés sont interdits.
Interdits notamment :
run_id
batch_id
product_id
offer_id
aw_product_id
url
path
query
sql
error_message
trace_id
Un identifiant utile à l'investigation reste dans les logs, les tables canoniques ou les traces de benchmark ; il ne devient pas automatiquement un label Prometheus.
feed¶
Le label feed est autorisé uniquement parce qu'il provient du registre canonique borné des feeds.
Il ne doit jamais recevoir une URL, un nom libre provenant du flux ou un identifiant créé par utilisateur.
labels à valeurs fermées¶
Le registre JSON borne également les valeurs de :
result;change_type;projection;job_type;query_class;route_class;status_class;operation;quantile.
Toute extension de ces ensembles exige une modification explicite du contrat.
Aucune nouvelle persistance métier¶
P1.5 ne crée pas une deuxième vérité.
Interdit :
- table SQL de métriques recopiant les runs ;
- table SQL de queue uniquement pour Prometheus ;
- stockage Redis durable de compteurs métier ;
- copie des IDs de run/produit/offre dans les labels ;
- recalcul métier dans Grafana ;
- calcul de capacité à partir d'un dashboard manuel non versionné.
Les métriques peuvent lire ou observer les frontières existantes, mais leur disparition ne doit pas supprimer la donnée métier durable.
Frontière de collecte¶
P1.5A ne choisit pas encore le mécanisme exact d'export applicatif.
P1.5B devra préférer la solution minimale qui respecte :
- lecture seule pour les gauges dérivées des tables ;
- instrumentation aux frontières canoniques pour les counters/histograms ;
- aucun bootstrap métier lourd lors d'un scrape ;
- aucune dépendance du Domain à Prometheus ;
- aucune obligation WordPress pour une métrique appartenant à la Platform ;
- échec de l'observabilité sans mutation fonctionnelle ;
- coût de collecte borné et mesuré.
P1.5B1 — événements Runtime sync¶
P1.5B1 ajoute une instrumentation structurée mais non encore exportée dans Prometheus.
Préfixe de log :
[CCX_CAPACITY_METRIC]
Quatre séries sont désormais instrumentées sur le chemin sync :
ccx_sync_source_rows_total
ccx_sync_normalized_rows_total
ccx_sync_persisted_rows_total
ccx_jobs_completed_total # sync_batch success path uniquement
Ces événements transportent un delta, des labels bornés et un contexte de déduplication séparé des labels Prometheus.
Le contexte peut contenir :
run_id
batch_index
offset_end
rows_seen_total
Ces champs sont des preuves opérationnelles et ne deviennent pas des labels Prometheus.
Frontière source rows¶
L'événement source n'est émis qu'après la persistance du checkpoint reader dans ccx_sync_runs_v2.
Cela évite de considérer un slice comme définitivement observé avant que le Runtime ait enregistré son avancement.
Frontière normalized / persisted¶
Les événements de batch sont émis après :
- persistence applicative réussie ;
- marquage des raw rows ;
- enregistrement du batch réussi via
finishBatch().
ccx_sync_persisted_rows_total{result="written"} utilise kept_items après le retour réussi de persist(). La distinction unchanged n'est pas encore instrumentée et ne doit pas être inventée.
Job completion¶
P1.5B1 instrumente uniquement :
job_type=sync_batch
result=success
Les erreurs batch, les rebuilds de projections et les autres familles de jobs restent explicitement hors scope de ce slice.
Sémantique de ces événements¶
Ces logs sont des delta events d'instrumentation.
Ils ne sont pas encore :
- un compteur Prometheus ;
- une source de vérité métier ;
- une garantie exactly-once ;
- une capacité VPS certifiée.
Un futur collector/exporter devra utiliser les clés de contexte bornées pour éviter les doubles comptes liés aux retries/crash windows avant de matérialiser des compteurs monotones.
Le registre machine-readable expose cet état via runtime_event_contract.live_export_status=NOT_PROMETHEUS_EXPORTED.
Une erreur de série, label ou delta métrique est rejetée avec le préfixe [CCX_CAPACITY_METRIC_REJECTED] et ne doit jamais interrompre la sync métier.
P1.5B2 — événements Runtime projections¶
P1.5B2 étend l'instrumentation structurée au refresh de projections déclenché par la finalisation sync.
Séries concernées :
ccx_projection_products_total
ccx_jobs_completed_total{job_type="projection_rebuild"}
Grain produit obligatoire¶
ccx_projection_products_total ne compte pas les lignes physiques des tables de projection lorsqu'une projection peut contenir plusieurs lignes par produit.
La correspondance canonique est :
| projection | compteur produit |
|---|---|
product_models |
rebuilt_models |
variants |
models_count |
specifications |
specs_count |
gallery |
gallery_count |
En particulier, rebuilt_variants est volontairement interdit comme compteur de produits projetés : plusieurs variantes peuvent appartenir au même modèle.
Seules les étapes dont le statut est ok émettent un delta result="success". Une projection unsupported_vertical ne fabrique aucun faux nombre de produits traités.
Frontière d'émission¶
L'instrumentation reste dans le sync runner, après retour réussi de ccx_sync_refresh_product_projections().
Les services transactionnels de write restent inchangés et ne dépendent pas du logger Runtime :
CCX_ProductModelProjectionWriteService
CCX_ProductVariantProjectionWriteService
CCX_ProductSpecificationProjectionWriteService
CCX_ProductGalleryProjectionWriteService
Cela maintient la séparation suivante :
projection write transaction
↓
projection refresh result
↓
runtime capacity event
Une erreur de métrique reste non bloquante et ne peut pas transformer une projection métier réussie en échec.
Si un refresh multi-vertical échoue après avoir déjà effectué des writes sur une verticale précédente, P1.5B2 émet le job projection_rebuild{result="error"} mais ne reconstruit pas des deltas produits partiels qui n'ont pas été retournés par la frontière de refresh. Un compteur approximatif serait pire qu'une sous-observation explicitement documentée.
Job projection_rebuild¶
Le compteur job utilise une exécution complète du refresh de projections du run sync comme unité :
success = ccx_sync_refresh_product_projections() retourne avec succès
error = le critical step refresh_product_projections échoue
Le contexte peut conserver run_id, feed et vertical_count, mais ces valeurs ne deviennent pas des labels.
P1.5B2 ne prétend pas couvrir les rebuilds manuels/CLI hors chemin Runtime sync. Le statut du registre reste donc explicitement limité au SYNC_REFRESH_PATH.
Toujours pas Prometheus live¶
Comme B1, B2 n'ajoute encore :
- aucun endpoint métrique ;
- aucun collector Prometheus ;
- aucun scrape target ;
- aucune recording rule ;
- aucune mutation VPS.
Les événements restent des preuves d'instrumentation structurées avec :
runtime_event_contract.live_export_status=NOT_PROMETHEUS_EXPORTED
P1.5B3 — échecs sync batch et gaps sémantiques¶
P1.5B3 ferme le chemin sync_batch{result="error"} et formalise deux métriques qui ne peuvent pas encore être instrumentées honnêtement.
sync_batch error¶
Un batch de travail non vide est considéré comme error uniquement si une exception remonte avant que finishBatch(status=success) ait retourné avec succès.
Le Runtime utilise un marqueur local :
batch_recorded_success=false
Puis :
exception avant durable success
→ ccx_jobs_completed_total{job_type="sync_batch",result="error"}
→ relance de la même exception
Une exception postérieure à la réussite durable du batch n'émet pas un second événement error, afin d'éviter de compter le même batch comme success et error.
Le contexte de l'événement peut contenir run_id, batch_index, feed et exception_class. Aucun de ces identifiants ne devient un label Prometheus.
Le comportement métier reste inchangé : l'exception originale est relancée.
offer_changes — instrumentation volontairement bloquée¶
Le chemin CURRENT ccx_feeds_upsert_items_to_offers_norm() retourne le nombre de lignes affectées par MySQL pour un :
INSERT ... ON DUPLICATE KEY UPDATE
Le code CURRENT documente lui-même :
MySQL affected rows for ON DUP can be misleading
Il est donc interdit de dériver ccx_offer_changes_total depuis :
norm_affected_rows;kept_items;final_rows;rows_ok.
La future frontière devra classer de vrais changements sémantiques commités :
created
updated
deleted
avec une déduplication retry-safe.
identity_products — instrumentation volontairement bloquée¶
Le pipeline CURRENT porte bien sur les offres :
product_identity_key
variant_identity_key
identity_level
identity_confidence
identity_reason_codes
Mais cette persistance row-level n'est pas une frontière de décision terminale au grain produit.
Il est donc interdit de dériver ccx_identity_products_total depuis :
- le nombre d'offres ;
rows_ok;kept_items;- les occurrences répétées de
product_identity_key.
La future frontière devra produire des décisions terminales bornées au grain produit, notamment :
resolved
unknown
ambiguous
conflict
avec sémantique retry-safe.
Règle fail-closed¶
P1.5B3 préfère un statut explicite BLOCKED_* à une série disponible mais fausse.
Le registre machine-readable porte désormais :
ccx_offer_changes_total
BLOCKED_SEMANTIC_DIFF_BOUNDARY_REQUIRED
ccx_identity_products_total
BLOCKED_PRODUCT_GRAIN_IDENTITY_BOUNDARY_REQUIRED
Ces blockers ne constituent pas un échec de B3 : ils constituent la preuve que la mesure ne sera pas fabriquée à partir de proxys incorrects.
P1.5B4A — frontières sémantiques offer-change et identity¶
P1.5B4A introduit une frontière pure et réutilisable :
CCX_CapacitySemanticBoundary
Elle ne lit ni SQL, ni WordPress, ni Prometheus.
Offer change¶
La classification canonique est :
before absent + after présent -> created
before présent + after absent -> deleted
before != after -> updated
before == after -> unchanged
Les snapshots sont normalisés par clé avant comparaison afin qu'un simple ordre différent des clés ne devienne pas un changement métier.
Le chemin CURRENT réellement câblé est limité à :
offer replay apply
et l'ordre obligatoire est :
write applicatif
→ relecture du snapshot canonique
→ COMMIT réussi
→ classification before/after
→ ccx_offer_changes_total{change_type="updated"}
Un dry-run n'émet rien.
Un second replay sans changement n'émet rien.
Un rollback n'émet rien.
Une panne du sink métrique après commit est absorbée : elle ne peut pas transformer un replay métier déjà commité en échec.
Les valeurs created et deleted sont définies dans la frontière et bornées dans le logger, mais ne sont pas encore prétendues câblées sur le sync normal.
Identity outcome¶
La frontière définit également la traduction métrique du vocabulaire terminal identity existant :
| statut identity | résultat métrique |
|---|---|
resolved |
resolved |
resolved_with_minor_divergence |
resolved |
unknown |
unresolved |
ambiguous |
unresolved |
invalid |
unresolved |
conflict |
conflict |
conflict_blocking |
conflict |
Le label ccx_identity_products_total{result=...} est donc désormais borné à :
resolved
unresolved
conflict
Mais aucun counter identity n'est encore émis.
Raison : le Runtime CURRENT ne possède toujours pas de frontière terminale exactly-once au grain produit. Les endroits suivants restent explicitement interdits pour incrémenter ce compteur :
- read-service identity ;
- review queue offer-level ;
- nombre de lignes d'offres ;
rows_ok;kept_items;- occurrences de
product_identity_key.
P1.5B4B devra raccorder cette traduction à une vraie matérialisation terminale product-grain, sans déplacer la décision métier dans l'observabilité.
Statut¶
P1.5B4A est une progression réelle mais partielle :
ccx_offer_changes_total:
RUNTIME_EVENT_INSTRUMENTED_OFFER_REPLAY_UPDATED_PATH_ONLY
ccx_identity_products_total:
SEMANTIC_BOUNDARY_DEFINED_RUNTIME_PRODUCT_GRAIN_REQUIRED
P1.5B ne peut donc pas encore être déclaré fermé.
P1.5B4B1 — diff sémantique des offres du sync normal¶
P1.5B4B1 étend ccx_offer_changes_total au chemin de finalisation d'une sync normale.
Le lot n'utilise toujours pas le nombre MySQL de lignes affectées.
Source canonique¶
La comparaison s'effectue directement sur :
ccx_offers_norm_v2
avec :
key = offer_key
scope = source_feed
Avant upsertNormalizedOffers(), le Runtime capture un snapshot temporaire de chaque offer_key et d'un hash des colonnes canoniques.
Après retour réussi de l'upsert — y compris son nettoyage des offres résiduelles — il classe :
clé absente avant, présente après -> created
clé présente avant et après, hash != -> updated
clé présente avant, absente après -> deleted
hash identique -> unchanged / aucun event
Hash sémantique¶
La liste de champs n'est pas recopiée manuellement.
Le service énumère les colonnes CURRENT de ccx_offers_norm_v2, trie leur nom et exclut uniquement les champs opérationnels :
offer_id
run_id
offer_key
raw_hash
row_hash
first_seen
last_seen
created_at
updated_at
run_id_last_seen
source_updated_at
Les champs métier restent donc dans le hash, notamment :
price
stock_status
product_identity_key
identity_confidence
review_required
vertical_id
...
Chaque valeur est encadrée avec un marqueur NULL/non-NULL et sa longueur avant SHA-256, afin d'éviter les collisions triviales de concaténation.
row_hash de ccx_offer_items n'est pas utilisé : son contrat est volontairement plus étroit et ne couvre pas tous les champs canoniques nécessaires à une mesure de changement d'offre.
Stockage temporaire¶
Le before-snapshot est conservé dans une table :
CREATE TEMPORARY TABLE tmp_ccx_offer_sem_<hash>
InnoDB, locale à la connexion, avec seulement :
offer_key BINARY(16)
semantic_hash BINARY(32)
Cette table :
- n'est pas une nouvelle persistance métier ;
- ne survit pas à la connexion ;
- n'est pas une table Prometheus ;
- est créée/supprimée par
CCX_TemporarySchemaService, pas par le sync runner.
Ordre d'exécution¶
capture before-snapshot
↓
upsertNormalizedOffers()
↓
cleanup canonique inclus dans l'upsert
↓
diff created / updated / deleted
↓
ccx_offer_changes_total
↓
refresh projections
Si le snapshot ou le diff échoue, la sync métier continue.
Si l'upsert métier échoue, le snapshot temporaire est supprimé et aucun événement de changement d'offre n'est émis.
Retry et crash-window¶
Un retry effectué après un upsert canonique complètement terminé voit normalement le nouvel état comme baseline et produit zéro diff. Cela évite le double comptage simple.
Mais P1.5B4B1 n'est pas exactly-once.
Crash critique :
upsert canonique réussi
→ crash avant calcul/émission du diff
→ perte du snapshot TEMPORARY
→ retry avec nouvel état en baseline
→ sous-comptage possible
Supprimer ce crash-window nécessiterait une preuve durable/outbox corrélée à la mutation canonique. P1.5B4B1 n'introduit pas une nouvelle table durable uniquement pour les métriques.
Le contexte des événements indique donc explicitement :
source=sync_finalize
crash_window=post_upsert_pre_emit_may_undercount
Couverture restante¶
Après B4B1 :
- replay d'offre commité : instrumenté ;
- sync normale created/updated/deleted : instrumentée ;
- writers canoniques directs hors sync/replay : pas encore tous instrumentés ;
- identity terminale au grain produit : toujours non câblée ;
- export Prometheus : toujours absent.
Le statut machine-readable reste volontairement explicite :
ccx_offer_changes_total:
RUNTIME_EVENT_INSTRUMENTED_SYNC_FINALIZE_AND_REPLAY_WITH_CRASH_WINDOW
runtime_event_contract.live_export_status:
NOT_PROMETHEUS_EXPORTED
P1.5B4B2A1 — writers taxonomy V2¶
P1.5B4B2A1 couvre les mutations directes de ccx_offers_norm_v2 effectuées par Taxonomy V2 en dehors du sync normal et de l'Offer Replay.
Les trois chemins couverts sont :
taxonomy-v2 apply
taxonomy-v2 rollback
taxonomy-v2 cleanup
Ils ne produisent que :
ccx_offer_changes_total{change_type="updated"}
car ces opérations ne créent ni ne suppriment d'offres canoniques.
Apply¶
Le snapshot Taxonomy V2 contient l'état ancien et la cible nouvelle par offer_id.
Avant l'UPDATE, le writer compte uniquement les offres dont l'état canonique courant diffère réellement de la cible :
category_norm
subcategory_norm
vertical_id
spec_type_final
classifier_reason_primary
Le delta métrique est ce nombre sémantique, pas le retour MySQL de l'UPDATE.
L'émission intervient uniquement après :
applySnapshotToOffers()
→ markSnapshotDone()
→ capacity event
Rollback¶
Le même principe est appliqué contre l'état old_* du snapshot.
Le delta est calculé avant mutation, puis émis seulement après :
restoreSnapshotToOffers()
→ createRollbackSnapshot()
→ capacity event
Un rollback déjà matériellement revenu à sa cible ne doit donc pas être compté à partir du simple nombre de lignes du snapshot.
Cleanup¶
ccx_taxonomy_v2_apply_cleanup_spec() fonctionne par batches transactionnels.
Pour chaque batch, le Runtime compte avant l'UPDATE les lignes sélectionnées par le même predicate, le même ordre et la même limite que le batch à muter.
semantic batch selection
→ UPDATE
→ COMMIT
→ callback capacity
Le callback n'est jamais appelé avant COMMIT.
Cette granularité batch évite de perdre tous les batches déjà committés si un batch ultérieur échoue.
Le compteur ne dépend ni de updated_total ni de affected_rows.
Séparation des couches¶
CCX_TaxonomyWriteService ne dépend pas de CCX_MetricsLogger.
Il reçoit un callback optionnel. L'interface CLI compose ce callback avec le logger Runtime.
Une exception du callback est absorbée après la mutation réussie : l'observabilité ne peut pas convertir une écriture taxonomy réussie en erreur métier.
Limite exactly-once¶
Comme pour les autres événements B4, il reste un crash-window post-write / pré-émission.
mutation réussie
→ crash avant event
→ sous-comptage possible
P1.5B4B2A1 ne prétend donc pas fournir de l'exactly-once.
Couverture restante après A1¶
Le registre couvre désormais :
- Offer Replay ;
- sync normale ;
- Taxonomy V2 apply ;
- Taxonomy V2 rollback ;
- Taxonomy V2 cleanup.
Le writer canonique encore explicitement non couvert est :
governance apply / governance rollback
→ ccx_governance_rebuild_offers_norm_for_run()
Ce chemin est traité séparément en P1.5B4B2A2, car son journal est au grain offer_items avant reprojection canonique.
L'identity terminale au grain produit reste un chantier distinct P1.5B4B2B : les tables de résolution opérateur et les projections groupées par product_identity_key ne sont pas le résultat terminal du Resolver Domain.
P1.5B4B2A2 — writers governance¶
P1.5B4B2A2 couvre les reprojections directes de ccx_offers_norm_v2 déclenchées par :
governance apply
governance rollback
La mutation governance se fait en deux temps :
UPDATE ccx_offer_items
→ COMMIT
→ ccx_governance_rebuild_offers_norm_for_run()
→ INSERT ... ON DUPLICATE KEY UPDATE ccx_offers_norm_v2
Le journal ccx_vertical_apply_run_items décrit les lignes intermédiaires offer_items. Il ne constitue donc pas le compteur canonique d'offres modifiées.
Snapshot ciblé par run¶
Juste avant le rebuild canonique, CCX_GovernanceOfferSemanticDiffService crée une table TEMPORARY :
tmp_ccx_gov_offer_sem_<hash>
Elle contient uniquement les offer_key reliées au run_uuid via :
ccx_vertical_apply_run_items
→ offer_item_id
→ ccx_offer_items.offer_key
Pour chaque clé, elle stocke :
offer_key
was_present
semantic_hash
was_present est nécessaire car un apply governance peut rendre une offre nouvellement éligible à la projection canonique.
Contrat de hash¶
A2 ne réimplémente pas le hash.
Le service réutilise directement :
CCX_OfferSemanticDiffService::semanticColumnsFrom()
CCX_OfferSemanticDiffService::semanticHashSql()
Le contrat est donc identique à B4B1 : les champs opérationnels sont exclus, tandis que les champs métier/classification présents dans ccx_offers_norm_v2 participent au hash.
Classification après rebuild¶
Après retour réussi de ccx_governance_rebuild_offers_norm_for_run(), le diff ciblé classe :
was_present=0 + ligne présente après → created
was_present=1 + hash différent → updated
was_present=1 + ligne absente après → deleted
sinon → unchanged / aucun event
Puis seulement :
ccx_offer_changes_total{change_type="created|updated|deleted"}
est émis.
Le retour SQL du rebuild reste disponible pour son usage historique, mais n'est jamais utilisé comme delta de capacité.
Sources explicitement interdites¶
Ne doivent pas alimenter ccx_offer_changes_total :
rebuild affected_rows
governance updated_count
governance run_items_count
nombre de lignes du journal offer_items
Ordre et isolation¶
Le wrapper Application suit :
offer_items COMMIT
→ targeted before-snapshot
→ canonical rebuild
→ targeted semantic diff
→ capacity event
Si le rebuild échoue :
snapshot TEMPORARY supprimé
→ aucun event
Si le snapshot ou le diff échoue alors que le rebuild canonique réussit, le résultat métier reste réussi et l'observabilité échoue fermée à zéro.
Une exception du sink métrique est également absorbée après réussite du rebuild.
CCX_GovernanceWriteService ne dépend pas de CCX_MetricsLogger. Le callback est composé dans runtime-admin.php, où le logger Runtime est déjà disponible.
Concurrence et exactly-once¶
Le snapshot est restreint aux seules offer_key du run, ce qui évite de compter les modifications sans rapport faites ailleurs dans le même feed.
Il ne constitue toutefois pas une isolation transactionnelle contre un writer concurrent sur la même offer_key entre capture et diff. Une telle course peut affecter l'attribution.
A2 conserve aussi le crash-window :
rebuild canonique réussi
→ crash avant émission
→ sous-comptage possible
Le contexte porte :
source=governance_apply|governance_rollback
semantic_boundary=governance_run_offer_keys
crash_window=post_rebuild_pre_emit_may_undercount
P1.5B4B2A2 n'est donc pas exactly-once.
Couverture des writers après A2¶
Pour la révision auditée, les frontières connues de mutation canonique sont maintenant couvertes :
- Offer Replay ;
- sync normale ;
- Taxonomy V2 apply ;
- Taxonomy V2 rollback ;
- Taxonomy V2 cleanup ;
- governance apply ;
- governance rollback.
Aucun writer direct connu de ccx_offers_norm_v2 ne reste hors instrumentation dans cette révision.
Le gap restant pour offer_changes est la durabilité/exactly-once. Toute introduction future d'un nouveau writer canonique impose de refaire l'inventaire.
L'identity terminale au grain produit reste séparément ouverte en P1.5B4B2B.
P1.5B4B2B1 — boundary terminale Identity¶
L'audit post-A2 confirme qu'aucune frontière automatique terminale au grain produit n'existait dans le Runtime CURRENT.
Les éléments suivants ne sont pas des autorités terminales automatiques :
ccx_canonical_products_v1: projection commerciale groupée ;ccx_manual_product_resolution_v1: décision opérateur ;plugins/ccx-feeds-industrial/includes/platform/domain-core/identity/*: logique legacy/transitoire marquée REWRITE/MIGRATE par ADR ;- les champs identity au grain offre ;
- la répétition d'un
product_identity_key.
P1.5B4B2B1 crée donc la boundary métier avant toute émission métrique.
Modèle Domain¶
Le modèle terminal vit dans :
src/Domain/Product/IdentityResolution/
Le vocabulaire Domain canonique est strictement borné à :
resolved
unknown
ambiguous
conflict
Le mot unresolved n'est pas un statut Domain. Il reste uniquement le label métrique borné qui agrège unknown et ambiguous.
ProductIdentityGroupResolver est pur, déterministe et indépendant de WordPress/SQL.
La policy B1 est volontairement conservatrice :
un EAN non vide unique et cohérent
→ resolved
plusieurs EAN non vides différents
→ conflict
subject_kind=ean mais EAN de preuve différent du sujet
→ conflict
sans EAN :
au moins deux sources indépendantes
+ même marque normalisée
+ même modèle normalisé
→ resolved
sans EAN :
plusieurs modèles complets plausibles pour la même marque
→ ambiguous
sans EAN :
marques complètes contradictoires
→ conflict
sinon
→ unknown
Une product_identity_key existante peut devenir en B2 une clé de regroupement pour des offres sans EAN, mais n'est jamais une preuve suffisante de résolution.
Fingerprints¶
Chaque décision produit deux SHA-256 déterministes :
evidence_fingerprint
decision_fingerprint
L'evidence fingerprint est indépendant de l'ordre d'entrée des observations.
Le decision fingerprint inclut :
- sujet ;
- statut terminal ;
- clé canonique éventuelle ;
- nombres de candidats/sources ;
- reason codes ;
- evidence fingerprint ;
- version du resolver.
Il change donc lorsqu'une vraie nouvelle base de preuve est finalisée, même si le label terminal reste identique.
Autorité durable¶
La nouvelle table métier canonique est :
ccx_product_identity_resolutions_v1
Elle est créée par la migration moderne :
component=identity-resolution
migration=IDENTITY-001
via le framework src/Application/SchemaMigration.
Cette table n'est pas une persistance de métriques. Elle matérialise l'état métier automatique terminal requis indépendamment de P1.5.
Une ligne est unique par :
subject_kind
+
SHA256(subject_key)
Elle conserve notamment :
resolution_status
canonical_identity_key
candidate_count
source_count
reason_codes_json
evidence_fingerprint
decision_fingerprint
resolver_version
generation
decided_at
Idempotence¶
WpdbIdentityResolutionStore utilise un UPSERT atomique conditionné par le decision_fingerprint.
retry avec même decision_fingerprint
→ generation inchangée
→ decided_at inchangé
→ aucun nouveau terminal generation
decision_fingerprint différent
→ generation + 1
→ nouvelle décision terminale persistée
Cette idempotence ne repose pas sur affected_rows.
Ce que B1 ne fait volontairement pas¶
P1.5B4B2B1 ne câble pas encore le sync Runtime.
Il ne :
- collecte pas encore les groupes de preuves depuis les offres ;
- n'exécute pas encore le resolver pendant le finalize ;
- n'émet aucun
ccx_identity_products_total; - ne crée pas encore l'outbox/event durable nécessaire à l'exactly-once métrique ;
- ne modifie aucun scrape Prometheus.
Après l'alignement B1A, le statut devient :
TERMINAL_MODEL_AND_STORE_CANONICAL_VOCABULARY_ALIGNED_RUNTIME_ORCHESTRATION_REQUIRED
Compatibilité de lecture : la brève révision B1 ayant persisté resolution_status=unresolved est relue comme unknown. Aucune nouvelle décision Domain ne persiste unresolved.
P1.5B4B2B1A — alignement du vocabulaire terminal¶
L'audit post-B1 a détecté une divergence entre l'implémentation et le contrat normatif docs/contracts/identity-resolver.md.
B1 avait momentanément réduit le vocabulaire Domain à :
resolved / unresolved / conflict
B1A corrige cette erreur de modélisation. L'autorité Domain expose désormais exactement :
resolved
unknown
ambiguous
conflict
et la frontière capacity conserve séparément :
resolved -> result="resolved"
unknown -> result="unresolved"
ambiguous -> result="unresolved"
conflict -> result="conflict"
Ainsi, l'observabilité peut rester volontairement basse cardinalité sans écraser les états d'incertitude métier.
P1.5B4B2B2A — outbox capacity durable¶
B2A ferme d'abord le crash-window entre la décision terminale durable et la création de son événement capacity.
La migration Platform :
component=identity-resolution
migration=IDENTITY-002
crée :
ccx_product_identity_capacity_outbox_v1
Cette table n'est ni une projection métier, ni une copie Prometheus. Elle est la frontière de transport durable entre une génération terminale Identity et sa future livraison au compteur capacity.
Une ligne est unique par :
resolution_id
+
generation
et conserve notamment :
resolution_status
decision_fingerprint
delivery_status
attempt_count
lease_token
leased_until
next_attempt_at
delivered_at
last_error
Le staging est atomique avec l'état terminal :
START TRANSACTION
→ UPSERT ccx_product_identity_resolutions_v1
→ relecture resolution_id + generation persistés
→ INSERT ccx_product_identity_capacity_outbox_v1\n ON DUPLICATE KEY UPDATE event_id=event_id
→ COMMIT
Ainsi :
retry avec même decision_fingerprint
→ même generation
→ même clé outbox
→ aucun doublon durable
nouvelle décision
→ generation + 1
→ exactement un nouvel event durable
échec du staging outbox
→ ROLLBACK
→ aucune nouvelle generation sans event durable
L'idempotence ne dépend pas de affected_rows.
La déduplication n'utilise pas INSERT IGNORE : seul le doublon attendu sur resolution_id + generation devient un no-op explicite via ON DUPLICATE KEY UPDATE event_id=event_id. Les autres erreurs SQL restent donc visibles et provoquent le rollback.
B2A n'émet toujours aucun ccx_identity_products_total.
Il ne collecte pas encore les preuves product-grain du Runtime et ne draine pas encore l'outbox. Le Domain reste indépendant du transport capacity et WpdbIdentityResolutionStore ne dépend ni de CCX_MetricsLogger, ni de Prometheus.
Le statut après B2A est donc :
TERMINAL_MODEL_AND_STORE_DURABLE_OUTBOX_READY_RUNTIME_ORCHESTRATION_REQUIRED
P1.5B4B2B2B — orchestration product-grain Runtime¶
B2B câble désormais le sync Runtime sur la boundary terminale certifiée, sans court-circuit métrique :
touched product-grain subjects
→ batched canonical evidence
→ ProductIdentityGroupResolver
→ FinalizeProductIdentityResolution
→ WpdbIdentityResolutionStore
→ terminal generation + durable outbox
Aucun ccx_identity_products_total n'est émis dans ce lot.
Activation schema-gated¶
Le factory Runtime ne devient actif que si les trois autorités existent :
ccx_offers_norm_v2
ccx_product_identity_resolutions_v1 (IDENTITY-001)
ccx_product_identity_capacity_outbox_v1 (IDENTITY-002)
Si les migrations Identity ne sont pas encore installées, le service retourne null, le chemin sync historique continue, et le résumé du run indique :
identity_resolution.runtime_status=schema_not_ready
B2B ne crée donc aucune dépendance implicite à un schéma non déployé.
Subjects touchés avant/après l'upsert¶
Pour ne pas full-scan tous les produits à chaque sync, le Runtime capture les subjects du feed :
avant upsert canonique
+
après upsert canonique
puis déduplique leur union.
Cela permet de traiter les produits :
- ajoutés ;
- modifiés ;
- encore présents ;
- retirés du feed courant.
Un subject qui existait avant mais n'a plus aucune preuve canonique actuelle est conservé comme historique terminal et n'est pas artificiellement réécrit en unknown.
Grain et priorité des subjects¶
La policy Runtime est :
ean_norm non vide
→ subject_kind=ean
→ subject_key=ean_norm
sinon, product_identity_key non vide
→ subject_kind=product_key
→ subject_key=product_identity_key
La product_identity_key legacy n'est donc qu'une clé de regroupement no-EAN. Elle n'est jamais une preuve positive de résolution.
Le Domain resolver reste seul responsable de conclure resolved / unknown / ambiguous / conflict.
Evidence canonique multi-source¶
WpdbIdentityEvidenceSource relit pour les subjects touchés toutes les offres canoniques actuelles, tous feeds confondus.
Les preuves utilisent uniquement des signaux présents dans l'offre canonique :
ean_norm
brand_norm
model_hint
title_clean
source_feed
merchant fallback
Les requêtes sont batchées par chunks, séparément pour :
- les subjects EAN ;
- les subjects
product_keysans EAN.
Le Runtime ne fait donc pas un SELECT par produit.
Déduplication des observations¶
Deux lignes d'offres produisant exactement la même observation normalisée ne doivent pas fabriquer deux preuves indépendantes.
Chaque payload IdentityEvidence normalisé est SHA-256 dédupliqué avant l'appel au resolver.
La source indépendante préférée est source_feed. Le merchant puis l'offer_key ne servent que de fallback si l'identité du feed est absente.
Ordre dans le finalize sync¶
L'ordre certifié devient :
capture identity subjects before
→ capture offer semantic snapshot
→ upsert ccx_offers_norm_v2
→ offer semantic diff
→ capture identity subjects after
→ finalize touched product identities
→ terminal state + outbox transaction
→ refresh product projections
Quand la boundary Identity est active, une erreur de collecte/finalisation est une erreur de finalize : un run ne doit pas être déclaré success si son autorité terminale Identity n'a pas pu être tenue à jour.
Frontières interdites¶
B2B ne dérive jamais une décision terminale depuis :
- le nombre de lignes d'offres ;
rows_okoukept_items;- une répétition de
product_identity_key; ccx_canonical_products_v1;ccx_manual_product_resolution_v1;- les previews/simulations legacy
platform/domain-core/identity; - la review queue.
Statut après B2B¶
terminal Domain model READY
durable terminal state READY
transactional capacity outbox READY
Runtime product-grain evidence READY
Runtime terminal finalization READY
capacity outbox delivery MISSING
ccx_identity_products_total NOT EMITTED YET
Il ne reste donc plus que P1.5B4B2B2C pour l'Identity capacity Runtime.
P1.5B4B2B2C — delivery durable capacity¶
B2C draine désormais uniquement les lignes durables de :
ccx_product_identity_capacity_outbox_v1
et traduit le vocabulaire Domain sans l'écraser :
resolved → result="resolved"
unknown → result="unresolved"
ambiguous → result="unresolved"
conflict → result="conflict"
Lease durable¶
WpdbIdentityCapacityOutbox sélectionne uniquement :
pending + next_attempt_at ready
OU
leased + leased_until expiré
dans une transaction :
START TRANSACTION
→ SELECT event_id ... FOR UPDATE
→ delivery_status=leased
→ attempt_count + 1
→ lease_token + leased_until
→ relecture des events loués
→ COMMIT
Un lease expiré est donc récupérable après crash.
L'ACK et le retry exigent tous deux :
event_id
+
lease_token courant
Une perte de propriété du lease est une erreur explicite.
Ordre de livraison¶
Pour chaque événement :
lease durable
→ mapping Domain → label capacity borné
→ ccx_identity_products_total delta=1
→ ACK durable
Le contexte de l'événement porte notamment :
event_id
resolution_id
generation
decision_fingerprint
terminal_status
delivery_attempt
delivery_semantics=at_least_once_stable_event_id
Le terminal store ne dépend toujours ni de CCX_MetricsLogger, ni du transport capacity.
Retry¶
Si le sink métrique échoue avant émission réussie :
delivery_status=pending
lease libéré
last_error renseigné
next_attempt_at = backoff exponentiel
Le backoff part de 15 secondes et est borné à une heure.
Si l'émission réussit mais que l'ACK durable échoue, le service ne force pas un retour immédiat à pending : le lease reste la frontière d'exclusion et l'événement redevient récupérable uniquement après expiration.
Triggers Runtime¶
Le drain est appelé :
- en best-effort pendant le finalize sync, après la finalisation terminale Identity ;
- par
CCX_SyncRunner::maintain(), afin de récupérer les pending et leases expirés même sans nouvelle décision ; - manuellement via
ccx sync maintain, avec--identity-capacity-limitoptionnel.
Une panne du delivery est une panne d'observabilité et ne transforme pas une finalisation métier valide en run failed.
Garantie réelle¶
L'outbox fournit l'exactly-once durable source boundary par :
resolution_id + generation
La livraison du log capacity est volontairement déclarée :
AT-LEAST-ONCE
Cas limite :
metric emitted
→ crash avant ACK
→ lease expire
→ même event_id livré à nouveau
B2C ne prétend donc pas à un exactly-once de bout en bout.
Le event_id stable permet au futur matérialisateur/exporter de dédupliquer explicitement s'il doit produire un compteur monotone exactly-once.
Statut après B2C¶
terminal Domain model READY
durable terminal state READY
transactional capacity outbox READY
Runtime product-grain evidence READY
Runtime terminal finalization READY
leased outbox delivery READY
ccx_identity_products_total RUNTIME EVENT WIRED
live Prometheus export NOT YET WIRED
P1.5B est alors fermé pour les événements Runtime.
La suite logique devient P1.5C — DB + frontend observability gaps / matérialisation live, sans prétendre que P1.5 complet est déjà certifié quantitativement.
Fermeture P1.5B — événements Runtime¶
P1.5B est CLOSED_PASS pour les événements Runtime sur la révision auditée :
main=9cc0462b75f9b14b1bcbf6fbbbec1cd04413d0eb
Les frontières Runtime suivantes sont instrumentées depuis leurs sources sémantiques canoniques :
ccx_offer_changes_total
ccx_sync_source_rows_total
ccx_sync_normalized_rows_total
ccx_sync_persisted_rows_total
ccx_identity_products_total
ccx_projection_products_total
ccx_jobs_completed_total
La fermeture B ne signifie pas que P1.5 global est terminé.
Restent volontairement dans P1.5C :
ccx_catalog_offers
ccx_db_query_duration_seconds
ccx_sync_queue_depth
ccx_sync_queue_lag_seconds
ccx_projection_lag_seconds
ccx_frontend_request_duration_seconds
ccx_frontend_http_requests_total
P1.5B certifie :
- les boundaries événementielles Runtime ;
- le grain produit terminal Identity ;
- l'outbox durable Identity ;
- la couverture des writers canoniques connus pour les changements d'offres ;
- les labels bornés et l'absence d'identifiants haute cardinalité dans les labels.
P1.5B ne certifie pas :
- l'export Prometheus live ;
- un transport end-to-end exactly-once ;
- la capacité quantitative du VPS ;
- les SLO DB/frontend ;
- les dashboards finaux.
Le statut machine-readable est :
status=RUNTIME_EVENTS_CERTIFIED_NOT_LIVE_EXPORTED
phase_status.p1_5b=CLOSED_PASS_RUNTIME_EVENTS
phase_status.p1_5c=NEXT_NOT_STARTED
phase_status.p1_5_global=OPEN
La suite logique est P1.5C — DB + frontend observability / live materialization gaps.
P1.5C1 — gauges DB read-only¶
P1.5C1 implémente les trois gauges dérivées de l'état canonique sans créer de nouvelle persistance métrique :
ccx_catalog_offers
ccx_sync_queue_depth
ccx_sync_queue_lag_seconds
La collecte est portée par :
CollectReadOnlyCapacityGauges
→ ReadOnlyCapacityGaugeSource
→ WpdbReadOnlyCapacityGaugeSource
La couche Infrastructure ne dépend ni de WordPress Runtime, ni de CCX_MetricsLogger.
Catalogue¶
ccx_catalog_offers est lu exclusivement depuis :
SELECT COUNT(*) FROM ccx_offers_norm_v2
Aucune table shadow ou copie de compteur n'est créée.
Queue depth¶
Le backlog canonique est défini par la même sémantique que le Runtime :
run.status ∈ pending | running | partial
raw.status ∈ pending | processing
Les lignes terminales ou appartenant à des runs non reprenables ne sont pas comptées.
Queue lag¶
Le lag utilise l'âge de la plus ancienne ligne encore exécutable/in-flight :
UTC_TIMESTAMP() - MIN(ccx_sync_stage_raw_v2.created_at)
Pour un feed borné sans backlog :
depth = 0
lag_seconds = 0
Cardinalité du feed¶
Le collector ne découvre pas librement des labels depuis la DB.
La liste des feeds autorisés est fournie par l'appelant depuis le registre canonique borné. Une ancienne valeur de DB qui n'appartient pas à cette liste est ignorée.
Lecture seule stricte¶
L'adapter wpdb C1 ne possède aucun chemin :
INSERT
UPDATE
DELETE
TRUNCATE
CREATE TABLE
ALTER TABLE
Une erreur SQL de collecte lève une erreur d'observabilité après nettoyage de $wpdb->last_error, sans mutation fonctionnelle.
Statut C1¶
Le registre machine-readable porte désormais :
ccx_catalog_offers
READ_ONLY_COLLECTOR_IMPLEMENTED_AWAITING_PROMETHEUS_EXPORT
ccx_sync_queue_depth
READ_ONLY_COLLECTOR_IMPLEMENTED_AWAITING_PROMETHEUS_EXPORT
ccx_sync_queue_lag_seconds
READ_ONLY_COLLECTOR_IMPLEMENTED_AWAITING_PROMETHEUS_EXPORT
P1.5C1 n'ajoute encore :
- aucun endpoint /metrics ;
- aucun scrape target Prometheus ;
- aucun sidecar/exporter ;
- aucune mutation VPS.
Le live export reste donc :
NOT_PROMETHEUS_EXPORTED
P1.5C2 — source native MariaDB pour latence et writes¶
P1.5C2 sélectionne une source MariaDB native capable de fournir une distribution réelle des temps de requête.
La source retenue est le plugin MariaDB :
query_response_time
Le choix évite deux faux raccourcis :
- une moyenne Performance Schema ne devient pas artificiellement un p95 ;
avg_ms_per_rowdu pipeline ne devient pas une métrique DB.
Configuration versionnée¶
Le dépôt contient :
docker/mariadb/observability.cnf
monté read-only dans MariaDB :
/etc/mysql/conf.d/zz-ccx-observability.cnf
avec :
plugin_load_add=query_response_time
query_response_time_stats=ON
query_response_time_range_base=2
Cette configuration n'est pas appliquée au VPS dans C2.
Classes de requêtes¶
Le collector lit les distributions MariaDB séparées :
QUERY_RESPONSE_TIME_READ → query_class="read"
QUERY_RESPONSE_TIME_WRITE → query_class="write"
Le label transaction reste réservé par le contrat mais n'est pas inventé en C2 faute de distribution transactionnelle équivalente.
Aucun des éléments suivants ne devient un label :
SQL text
digest
digest text
table name
URL
query string
Histogramme¶
Chaque ligne MariaDB fournit un intervalle :
TIME
COUNT
TOTAL
C2 transforme les COUNT par intervalle en buckets cumulatifs compatibles avec un futur histogramme Prometheus.
Le bucket TOO LONG est accepté uniquement avec COUNT=0. Si ce bucket devient non nul, C2 refuse de fabriquer un _sum puisque MariaDB ne fournit pas de durée exacte pour ce bucket.
Avec query_response_time_range_base=2, la plage finie couvre très largement les durées opérationnelles attendues.
Connexion dédiée¶
Le collector exige :
@@SESSION.query_response_time_session_stats = OFF
La future connexion d'export C4 devra être dédiée à l'observabilité.
Cette règle empêche les SELECT de collecte de contaminer eux-mêmes la distribution observée.
DB writes¶
Le contrat DB_writes_per_sec est désormais relié à des compteurs MariaDB vérifiables :
Handler_write
Handler_update
Handler_delete
Ils représentent des requêtes de write/update/delete au storage engine et excluent leurs équivalents de tables temporaires.
C2 additionne ces compteurs cumulatifs comme source de DB row writes total. Le rate Prometheus sera matérialisé plus tard.
Statut C2¶
ccx_db_query_duration_seconds
MARIADB_QUERY_RESPONSE_TIME_SOURCE_CONFIGURED_AWAITING_PROMETHEUS_EXPORT
P1.5C2 ne crée encore :
- aucun endpoint Prometheus ;
- aucun exporter ;
- aucune recording rule ;
- aucun dashboard ;
- aucune mutation live.
P1.5C3 — projection lag corrélé¶
P1.5C3 instrumente ccx_projection_lag_seconds sans approximation par timestamps globaux.
La frontière de départ est prise dans le même finalize sync, immédiatement après le retour réussi de upsertNormalizedOffers() :
canonical offer upsert committed
→ hrtime(true)
Le Runtime n'active cette mesure que si le diff sémantique canonique prouve au moins un :
created
updated
deleted
Si le diff est indisponible ou si le run est un no-op sémantique, aucun lag n'est inventé.
Completion boundary¶
CCX_ProductModelRebuildOrchestrator enregistre un timestamp monotone juste après le retour de chaque étape :
models
variants
specifications
gallery
Le calcul pur CCX_ProjectionLagCorrelator corrèle ces timestamps au timestamp post-commit du même finalize.
Pour plusieurs verticales affectées, la série bornée par projection conserve le maximum du lag observé pour ce type de projection. Cela représente le temps nécessaire pour que toutes les instances affectées de cette projection aient terminé.
Les étapes unsupported_vertical ne produisent pas de lag.
Horloge¶
La mesure utilise :
PHP hrtime(true)
donc une horloge monotone. Elle ne dépend ni de l'horloge murale du DB server, ni d'un décalage NTP, ni des colonnes updated_at.
Frontières interdites¶
P1.5C3 interdit explicitement :
- la soustraction de deux
MAX(updated_at)globaux sans corrélation ; - le timestamp maximum d'une table de projection comme approximation du lag du run courant ;
- la durée totale du sync comme substitut du lag projection.
Emission¶
Après refresh réussi :
ccx_projection_lag_seconds{projection="product_models|variants|specifications|gallery"}
est émis sous forme de gauge_observation structurée.
run_id, feed, lag_source et la portée de corrélation restent dans le contexte et pas dans les labels Prometheus.
Une panne du sink observabilité ne transforme jamais un refresh projection réussi en échec métier.
Statut C3¶
ccx_projection_lag_seconds
CORRELATED_SYNC_REFRESH_INSTRUMENTED_AWAITING_PROMETHEUS_EXPORT
Le live export reste :
NOT_PROMETHEUS_EXPORTED
P1.5C4 — frontend request observability¶
P1.5C4 ferme les deux gaps d'instrumentation frontend :
ccx_frontend_request_duration_seconds
ccx_frontend_http_requests_total
La frontière choisie est le cycle de requête frontend public, pas un template ou un renderer particulier.
Le stack est activé uniquement lorsque :
ccx_feeds_should_load_frontend_stack() = true
Sont donc exclus avant instrumentation :
- WP-CLI ;
- CLI PHP ;
- cron ;
- AJAX ;
- REST ;
- admin ;
- feed enqueue/worker ;
- health endpoint.
Horloge¶
Le début de mesure est capturé dans le bootstrap frontend avec :
hrtime(true)
La fin est capturée dans un callback shutdown.
La durée utilise donc une horloge monotone et ne dépend ni de l'horloge murale, ni du DB server, ni de timestamps de contenu.
route_class¶
Le label est strictement borné :
home
catalog
product
other_public
Classification :
/ -> home
/produit/{ean}/ -> product
/produits/{slug}/ -> product
route contract kind=catalog -> catalog
route contract kind=product_model -> product
reste du frontend public -> other_public
Aucun EAN, slug, path, URL ou product ID ne devient un label ou un contexte métrique.
status_class¶
Le compteur HTTP utilise uniquement :
2xx
3xx
4xx
5xx
Le code HTTP brut n'est pas un label.
Un status non initialisé/invalide est traité comme le status frontend par défaut 200/2xx.
Émission¶
À la fin de chaque requête publique instrumentée :
ccx_frontend_request_duration_seconds{route_class="..."}
histogram_observation
ccx_frontend_http_requests_total{
route_class="...",
status_class="..."
}
counter_delta=1
La durée et le compteur sont émis indépendamment. Une panne de l'un des sinks n'altère jamais la réponse publique déjà produite.
Séparation des couches¶
FrontendRequestObservation vit dans src/Application/Observation/Frontend et contient uniquement :
- vocabulaire borné ;
- classification ;
- conversion de status ;
- calcul monotone de durée.
L'adapter WordPress frontend gère :
REQUEST_URIuniquement pour classification locale ;http_response_code();register_shutdown_function().
Le logger concret est composé dans le bootstrap frontend. L'adapter ne dépend pas directement de CCX_MetricsLogger.
Statut C4¶
ccx_frontend_request_duration_seconds
FRONTEND_REQUEST_HISTOGRAM_INSTRUMENTED_AWAITING_PROMETHEUS_EXPORT
ccx_frontend_http_requests_total
FRONTEND_HTTP_COUNTER_INSTRUMENTED_AWAITING_PROMETHEUS_EXPORT
Le live export reste :
NOT_PROMETHEUS_EXPORTED
Avec C4, les gaps d'instrumentation applicative de P1.5C sont fermés. La suite de P1.5C doit matérialiser les sources/événements CURRENT dans une exposition Prometheus bornée et certifiable, sans introduire une nouvelle vérité métier.
P1.5C1A — autorité de collecte Alloy¶
Après l'implémentation des sources C1–C4, P1.5C1A remplace l'agent de collecte de logs actif sans modifier la sémantique de ces mesures.
L'autorité active devient :
Grafana Alloy v1.19.2
image manifest:
sha256:b8ec653c44235fbe910879145dac3597d66b0aaecf60bcbbe82580767771a839
Le lot conserve à parité les quatre sources de logs existantes :
Docker json-file logs
/var/log/auth.log
/var/log/syslog
/var/log/kern.log
et les envoie toujours à Loki.
Promtail reste uniquement une autorité legacy de positions pour le premier démarrage :
legacy_positions_file=/var/lib/promtail/positions.yaml
Son state est monté en lecture seule. Le state Alloy est séparé sous /mnt/data/cmonchoix-infra/observability/alloy.
Le répertoire legacy Promtail doit exister pour que le bind mount reste explicite avec create_host_path: false. Le fichier positions.yaml, lui, peut légitimement être absent sur un hôte où aucune position durable n'a été matérialisée. Dans ce cas :
preflight state = MISSING_RESTART_FROM_END
legacy mutation = NO
Alloy fallback = on_positions_file_error=restart_from_end
Le preflight ne crée jamais un faux positions.yaml. S'il existe, le fichier doit être régulier, non symlinké et lisible ; sinon le déploiement échoue fermé.
Les deux paths utilisent create_host_path: false afin que Docker ne crée jamais silencieusement un state host vide.
Limite volontaire de C1A¶
C1A ne contient aucun matérialisateur de métriques métier.
Donc :
logs → Alloy → Loki PREPARED
C1–C4 sources READY
ccx_* → Prometheus NOT YET MATERIALIZED
monitoring/promtail.yml reste versionné uniquement comme référence legacy/rollback ; le Compose actif ne démarre plus Promtail.
La suite logique est la matérialisation Prometheus bornée des événements Runtime et des collectors read-only déjà certifiés.
P1.5C1B — matérialisation Prometheus des événements Runtime via Alloy¶
C1B matérialise les événements structurés [CCX_CAPACITY_METRIC] déjà certifiés sans introduire une seconde vérité métier.
Le chemin Git-configuré devient :
boundary métier canonique
→ CCX_MetricsLogger
→ stderr container json-file
→ Alloy loki.process
→ stage.metrics
→ Alloy /metrics
→ Prometheus scrape job=alloy
Le transport capacity est désormais always-on : il écrit directement vers stderr même lorsque WP_DEBUG est désactivé. Les logs Runtime ordinaires restent soumis au comportement historique de ccx_feeds_log().
Séries matérialisées par C1B¶
ccx_offer_changes_total
ccx_sync_source_rows_total
ccx_sync_normalized_rows_total
ccx_sync_persisted_rows_total
ccx_identity_products_total
ccx_projection_products_total
ccx_jobs_completed_total
ccx_projection_lag_seconds
ccx_frontend_request_duration_seconds
ccx_frontend_http_requests_total
C1B ne matérialise pas encore :
ccx_catalog_offers
ccx_db_query_duration_seconds
ccx_sync_queue_depth
ccx_sync_queue_lag_seconds
Ces séries proviennent de collectors read-only/DB et gardent une frontière d'export séparée.
Cardinalité Alloy¶
La source Docker fan-out vers deux branches :
docker logs
├─ C1A normal branch → Loki (inchangée)
└─ C1B capacity sidecar → stage.metrics → drop sidecar copy
Chaque série possède un stage.match dédié dans le sidecar. Avant stage.metrics, un stage.label_keep ne conserve que les labels autorisés dans le registre.
Le sidecar est ensuite droppé et n'est jamais envoyé à Loki. La branche C1A conserve donc ses labels d'origine, y compris le filename fourni par loki.source.file.
Les labels Docker, filename, chemins, container IDs et champs de contexte ne peuvent pas fuiter vers les séries ccx_*.
Le feed conserve en plus son contrôle syntaxique historique et, lorsque l'autorité de déploiement est disponible, doit appartenir à ccx_runtime_enabled_feeds().
Endpoint et scrape¶
Alloy expose les custom metrics sur son endpoint HTTP interne déjà présent :
http://alloy:12345/metrics
Aucun port hôte supplémentaire n'est ouvert.
Prometheus ajoute :
job_name=alloy
target=alloy:12345
metric_relabel keep __name__=ccx_.*
Les métriques internes Alloy ne sont donc pas ingérées par ce job, tandis que up{job="alloy"} reste disponible comme santé de scrape.
Types¶
Counters :
stage.metrics metric.counter
source=delta
action=add
prefix=""
Gauge :
ccx_projection_lag_seconds
source=value
action=set
Histogram :
ccx_frontend_request_duration_seconds
buckets seconds:
0.005 0.01 0.025 0.05 0.1 0.25 0.5 1 2.5 5 10
Les séries custom restent présentes jusqu'à max_idle_duration=168h sans activité.
Sémantique de restart/reload¶
Les custom metrics stage.metrics sont en mémoire Alloy et peuvent repartir à zéro lors d'un restart/reload de configuration.
C1B ne transforme donc pas ces counters en journal durable.
Prometheus conserve l'historique de scrape et doit interpréter une baisse comme un reset normal du counter pour rate() / increase().
Les garanties source restent inchangées :
- identity reste at-least-once avec
event_idstable ; C1B ne déduplique pasevent_id; - offer changes conserve ses crash-windows non exactly-once ;
- C1B ne prétend pas améliorer ces garanties.
Statut¶
C1B est un lot Git/off-live.
Prometheus materialization config READY IN GIT
Alloy /metrics live NOT LIVE CERTIFIED
Prometheus live scrape NOT LIVE CERTIFIED
VPS mutation NONE
Le champ status des sources continue de décrire leur frontière métier. C1B ajoute séparément prometheus_materialization_status=GIT_CONFIGURED_VIA_ALLOY_NOT_LIVE_CERTIFIED.
La prochaine étape de P1.5C est l'exposition des collectors read-only et MariaDB déjà certifiés, pas une nouvelle instrumentation métier.
Normalisation du namespace Alloy vers Prometheus¶
Le Runtime Alloy expose les métriques créées par stage.metrics sous son namespace interne
loki_process_custom_. Le contrat CMonChoix reste toutefois défini sur les noms canoniques
ccx_*.
La frontière Prometheus doit donc appliquer, sur le job alloy, les opérations dans cet ordre :
loki_process_custom_ccx_*
↓ metric_relabel replace
ccx_*
↓ metric_relabel keep
Prometheus
La normalisation ne change ni les événements Runtime, ni leurs labels métier, ni les recording
rules P1.5D. Elle retire uniquement le namespace d'exposition propre à Alloy avant ingestion
Prometheus et conserve ensuite exclusivement les familles ccx_*.
Cette frontière a été ajoutée à l'autorité Git après l'audit live P1.5F qui a établi simultanément :
- les événements
[CCX_CAPACITY_METRIC]présents dans lejson-fileDocker ; - leur fichier source visible et suivi par Alloy ;
- les familles matérialisées sur
/metricssousloki_process_custom_ccx_*; - le scrape Prometheus du job
alloyactif mais filtrant ces familles avant ingestion.
Le correctif reste Git-only tant qu'il n'a pas été déployé et recertifié sur le VPS.
P1.5C1C — matérialisation des gauges read-only¶
P1.5C1C raccorde les collectors C1 déjà certifiés au transport Prometheus C1B sans dupliquer leur SQL.
La chaîne devient :
systemd timer (repo-managed, 5 min)
→ wp ccx sync capacity-gauges
→ CCX_ReadOnlyCapacityGaugeDelivery
→ CollectReadOnlyCapacityGauges
→ WpdbReadOnlyCapacityGaugeSource
→ CCX_MetricsLogger gauge_observation
→ Docker stderr/json-file
→ Alloy capacity_metrics sidecar
→ gauge/set
→ Prometheus job=alloy
Les séries sont :
ccx_catalog_offers
ccx_sync_queue_depth{feed}
ccx_sync_queue_lag_seconds{feed}
Le bridge Runtime ne contient aucun SQL. La définition canonique reste dans WpdbReadOnlyCapacityGaugeSource.
La liste des feeds vient exclusivement de :
ccx_runtime_enabled_feeds()
et le logger applique la même autorité bornée aux labels feed que pour les counters C1B.
Scheduler¶
Les artefacts versionnés sont :
operations/systemd/ccx-capacity-gauges.service
operations/systemd/ccx-capacity-gauges.timer
Le timer utilise :
OnBootSec=2min
OnUnitInactiveSec=5min
AccuracySec=15s
Il n'utilise pas Persistent=true : une gauge représente l'état courant et ne doit pas rejouer des mesures historiques après une indisponibilité.
Le WP-Cron Runtime reste désactivé. C1C ne réactive pas une ancienne autorité de scheduling.
Staleness¶
Ces trois gauges utilisent :
max_idle_duration=15m
dans Alloy.
Si le scheduler/collector cesse de fonctionner, une ancienne valeur n'est donc pas conservée pendant 168 heures comme si elle était fraîche.
État de déploiement¶
C1C est uniquement configuré/certifié en Git :
prometheus_materialization_status =
GIT_CONFIGURED_VIA_RUNTIME_ALLOY_NOT_LIVE_CERTIFIED
Le lot ne :
- n'installe ni n'active le timer systemd sur le VPS ;
- ne redémarre aucun container ;
- ne recharge pas Alloy/Prometheus ;
- ne modifie aucune donnée métier ;
- ne change aucune définition SQL du collector C1.
P1.5C2B — matérialisation Prometheus DB exacte¶
P1.5C2B matérialise les deux sorties DB déjà certifiées par C2 sans transformer la distribution MariaDB en observations artificielles.
Chaîne d'autorité¶
MariaDB query_response_time READ/WRITE
+ GLOBAL_STATUS Handler_write/update/delete
↓
collector read-only dédié
↓
Prometheus classic text exposition
↓
/mnt/data/cmonchoix-infra/observability/textfile/ccx-db-observability.prom
↓
node-exporter textfile collector
↓
Prometheus job=node
Aucune nouvelle cible Prometheus n'est créée.
Node exporter réutilise son rootfs existant monté en lecture seule et lit :
/host/mnt/data/cmonchoix-infra/observability/textfile
Histogramme de latence¶
Le collector C2 fournit déjà des buckets cumulatifs, un count et une somme en secondes.
C2B les conserve exactement sous la forme Prometheus classique :
ccx_db_query_duration_seconds_bucket{query_class="read|write",le="..."}
ccx_db_query_duration_seconds_count{query_class="read|write"}
ccx_db_query_duration_seconds_sum{query_class="read|write"}
Il est interdit de convertir ces buckets en une série d'observations synthétiques pour Alloy : une telle reconstruction modifierait la distribution et rendrait les quantiles p95/p99 non fiables.
Le vocabulaire de label reste strictement borné :
query_class = read | write
SQL, digest, table, URL et path restent interdits.
Compteur de writes DB¶
Le collector publie également :
ccx_db_row_writes_total
dont la valeur cumulative est :
Handler_write
+ Handler_update
+ Handler_delete
La sortie capacity devient donc :
rate(ccx_db_row_writes_total[5m])
Prometheus gère les resets normaux de compteur lors d'un redémarrage MariaDB.
Session d'observation dédiée¶
Avant toute collecte, le Runtime exécute sur la session du collector :
SET SESSION query_response_time_session_stats=OFF
La requête d'observation elle-même n'alimente donc pas les buckets query_response_time.
Le bridge reste read-only vis-à-vis des données métier et ne dépend pas de CCX_MetricsLogger.
Publication atomique¶
Le writer hôte :
- écrit dans un fichier temporaire dans le même répertoire ;
- vérifie que les familles histogram/count/sum/counter attendues existent ;
- exécute
promtool check metricsavec le binaire de l'image Prometheus versionnée ; - applique le mode
0644; - remplace atomiquement le fichier final par
mv.
Une collecte DB ou une validation qui échoue ne remplace donc jamais la dernière exposition valide.
État textfile¶
Le répertoire :
/mnt/data/cmonchoix-infra/observability/textfile
doit être préparé explicitement via :
bash scripts/observability-textfile-state-prepare.sh
Le preflight vérifie notamment que /mnt/data est bien un filesystem distinct et refuse un path symlink.
Scheduling¶
La collecte est versionnée comme oneshot systemd :
ccx-db-observability.service
ccx-db-observability.timer
Cadence :
OnBootSec=3min
OnUnitInactiveSec=5min
AccuracySec=15s
Le démarrage à 3 minutes est volontairement décalé du timer read-only capacity gauges qui démarre à 2 minutes.
Ce lot Git :
- n'installe ni n'active le timer ;
- ne redémarre pas node-exporter ;
- ne recharge pas Prometheus ;
- ne modifie aucune donnée métier ;
- ne certifie aucune série live.
Statut :
ccx_db_query_duration_seconds
GIT_CONFIGURED_VIA_NODE_EXPORTER_TEXTFILE_NOT_LIVE_CERTIFIED
ccx_db_row_writes_total
GIT_CONFIGURED_VIA_NODE_EXPORTER_TEXTFILE_NOT_LIVE_CERTIFIED
P1.5D — recording rules capacité et dashboard SLO¶
P1.5D ne crée aucune nouvelle métrique source. Il matérialise exclusivement les dérivations déjà définies par P1.5A au-dessus des sources C1–C4 rendues disponibles en Git par P1.5C.
Recording rules¶
Le groupe Prometheus :
cmonchoix-capacity-recording
produit les 19 séries contractuelles :
ccx_capacity_total_offers
ccx_capacity_offers_changed_24h
ccx_capacity_source_rows_per_second
ccx_capacity_normalized_rows_per_second
ccx_capacity_persisted_rows_per_second
ccx_capacity_identity_products_per_second
ccx_capacity_projection_products_per_second
ccx_capacity_jobs_per_minute
ccx_capacity_db_writes_per_second
ccx_capacity_db_query_latency_p95_seconds
ccx_capacity_worker_cpu_ratio
ccx_capacity_worker_memory_bytes
ccx_capacity_disk_iops
ccx_capacity_queue_depth
ccx_capacity_queue_lag_seconds
ccx_capacity_projection_lag_seconds
ccx_capacity_frontend_p95_seconds
ccx_capacity_frontend_p99_seconds
ccx_capacity_http_5xx_ratio
Toutes ces sorties sont des agrégats sans label. Les labels métier bornés des métriques sources et les labels techniques de scrape ne sont pas propagés dans les séries ccx_capacity_*.
La fenêtre standard reste :
5m
Pour offers_changed_24h, la fenêtre contractuelle reste explicitement 24h.
Le ratio HTTP utilise un zéro explicite pour l'absence de série 5xx lorsqu'un trafic total existe :
(sum(rate(ccx_frontend_http_requests_total{status_class="5xx"}[5m])) or vector(0))
/
clamp_min(sum(rate(ccx_frontend_http_requests_total[5m])), 1)
Aucun code HTTP brut ni route dynamique n'est réintroduit.
Dashboard¶
Le dashboard versionné :
monitoring/grafana/dashboards/platform/capacity-slo.json
uid=ccx-capacity-slo
title=Capacity & SLO
consomme uniquement les séries ccx_capacity_*.
Grafana n'y recalcule donc ni SQL, ni règles métier, ni formules de capacité parallèles. Les dérivations PromQL restent centralisées dans les recording rules.
Le dashboard couvre notamment :
- volumes d'offres ;
- débit source/normalisé/persisté ;
- débit Identity/projections ;
- jobs/minute ;
- CPU/RAM worker ;
- DB writes et p95 DB ;
- IOPS disque ;
- queue depth/lag ;
- projection lag ;
- frontend p95/p99 ;
- ratio HTTP 5xx.
Design targets¶
P1.5D ne transforme pas une intuition en SLO certifié.
Seuls trois objectifs déjà versionnés dans performance.md deviennent des alertes de design target :
frontend p95 > 300 ms
HTTP 5xx ratio > 0.1 %
projection lag > 300 s
Le label d'alerte :
objective=design_target_unvalidated
rappelle que ces seuils doivent encore être validés quantitativement par P1.5F/G/H.
Aucun seuil n'est inventé pour :
CPU
RAM
DB latency
DB writes
disk IOPS
queue depth
queue lag
throughput
Ces seuils dépendent de la baseline et du benchmark réel.
État de déploiement¶
P1.5D reste Git/off-live :
recording rules GIT CONFIGURED
dashboard GIT CONFIGURED
design-target alerts GIT CONFIGURED
Prometheus live NOT LIVE CERTIFIED
Grafana live NOT LIVE CERTIFIED
VPS mutation NONE
P1.5C peut être considéré fermé pour la matérialisation Git des sources obligatoires, pas pour leur présence live.
P1.5E versionne désormais le harness reproductible. La prochaine étape après certification Git de ce harness est P1.5F : baseline production read-only.
Autorité systemd live des collectors P1.5¶
Les timers C1C/C2B ne doivent pas dépendre du checkout historique /mnt/data/cmonchoix-platform.
L'autorité d'installation est désormais :
ops/systemd/capacity-observability-systemd-artifacts.manifest
scripts/capacity-observability-systemd-install.sh
Le manifeste contient exactement cinq artefacts :
1 helper
collect-db-observability-prometheus.sh
4 unités
ccx-capacity-gauges.service
ccx-capacity-gauges.timer
ccx-db-observability.service
ccx-db-observability.timer
Le helper DB est installé dans :
/usr/local/libexec/cmonchoix-capacity/
Les unités sont installées dans :
/etc/systemd/system/
Le service gauge exécute directement le worker Docker et ne requiert aucun working directory Git.
Le service DB exécute uniquement le helper installé dans /usr/local/libexec et déclare explicitement RequiresMountsFor=/mnt/data, car le helper écrit le textfile Prometheus sous cette autorité de stockage.
Contrat d'installation¶
L'installateur exige :
CCX_TARGET_SHAexact sur 40 caractères ;CCX_RELEASE_ROOTpropre et au HEAD exact ;- manifeste et sources suivis par Git ;
- aucune source/cible sous forme de symlink ;
- validation
bash -ndu helper ; - vérification exacte des fichiers installés avant reload ;
systemctl daemon-reloadsans activation de service/timer ;- validation des quatre unités via le manager systemd chargé (
LoadState+FragmentPath) ; - validation de
mnt-data.mountvia le manager live (LoadState=loaded,active,Where=/mnt/data) ; - validation des dépendances
Requires/AfterDocker et du mount DB après reload.
La vérification offline systemd-analyze verify est volontairement interdite ici : mnt-data.mount provient de systemd-fstab-generator et n'appartient pas à l'autorité offline utilisée par cet outil, ce qui peut produire un faux Unit mnt-data.mount not found.
Les substitutions sont atomiques et les cibles précédentes sont sauvegardées avant la première mutation.
Tout échec après armement déclenche un rollback des cinq cibles et un daemon-reload de récupération.
Aucun changement de planning implicite¶
L'installateur peut appeler uniquement :
systemctl is-enabled
systemctl is-active
systemctl show
systemctl daemon-reload
Il lui est interdit d'appeler :
start
stop
restart
enable
disable
mask
unmask
Les états enabled/active des deux timers sont capturés avant installation.
Pour une unit déjà présente, l'état doit rester strictement identique.
Pour une première installation où la unit est absente, la transition suivante est considérée comme équivalente du point de vue du scheduling :
is-enabled: not-found -> disabled
Le contrat se fonde sur l'état textuel retourné par systemd, pas sur un code retour numérique figé. En particulier, un vrai systemctl is-enabled peut retourner not-found avec un code distinct de celui de disabled (par exemple 4 contre 1). Cette différence de code retour ne constitue pas une activation.
Elle matérialise uniquement le fichier unit sans l'activer. Toute transition vers enabled, enabled-runtime, linked, masked ou un état actif reste interdite.
L'état textuel is-active doit rester strictement inchangé. Le code retour numérique n'est pas une autorité de scheduling lorsqu'il ne fait qu'évoluer avec la matérialisation de l'unit. Une première installation peut donc observer inactive avant et après avec des codes retour différents (par exemple unit absente puis unit chargée) sans constituer une activation. Toute transition textuelle vers active, activating, deactivating, failed ou un autre état différent reste interdite.
L'activation live des timers reste donc une décision opérateur séparée, postérieure à l'installation et à la validation du stack P1.5.
Marqueurs¶
Une installation certifiée localement retourne :
CCX_CAPACITY_SYSTEMD_INSTALL_CHANGED=0|1
CCX_CAPACITY_SYSTEMD_INSTALL_TARGET_SHA=<sha>
CCX_CAPACITY_SYSTEMD_INSTALL_STATUS=PASS
Ce contrat prépare le rollout live ; il ne constitue pas à lui seul une activation des collectors ni une certification P1.5F.
P1.5E — harness de benchmark reproductible¶
P1.5E transforme le contrat de benchmark en exécutable versionné, sans certifier encore une capacité quantitative du VPS.
Autorités :
benchmarks/capacity/harness-contract.json
scripts/capacity-benchmark-harness.sh
Le mode par défaut est strictement :
--plan
Il valide le scénario et les métadonnées sans lancer de workload, sans contacter Docker et sans écrire un résultat de benchmark.
Une exécution réelle exige explicitement :
--execute
--expected-git-sha <SHA exact>
--workload-script <fichier exécutable>
--verifier-script <fichier exécutable>
Le worktree doit être propre et le SHA courant doit être exactement celui annoncé.
Le harness n'interprète aucune chaîne de commande dynamique. eval et bash -c sont interdits. Le workload et le verifier sont des fichiers exécutables séparés et leurs SHA-256 sont conservés dans le manifest.
Répétitions¶
Le contrat impose :
1 warm-up minimum
3 measured runs minimum
Le résumé conserve au minimum la durée médiane, le pire run et la preuve business_correctness=PASS.
Preuves figées¶
Le manifest conserve notamment :
git_sha
worker_image_reference
worker_image_id
config_hashes
dataset_id
dataset_sha256
offers
products
merchant_count
change_percent
worker_count
scenario_id
benchmark_harness_version
started_at
finished_at
Les fichiers Compose, Alloy, Prometheus, recording rules, contrat capacité et dashboard sont hashés avant exécution.
Les résultats sont écrits par défaut hors du dépôt :
/mnt/data/cmonchoix-benchmarks/p1-5
Mesures par run¶
Chaque répétition suit :
runtime state before
capacity before
workload
business verifier
capacity after
capacity max/avg window
runtime state after
Les 19 séries ccx_capacity_* de P1.5D sont interrogées directement via l'autorité Prometheus.
Le harness capture aussi l'état des containers critiques, notamment :
status
restart_count
OOMKilled
started_at
avant et après le workload.
Un workload ou verifier non nul rend le run FAIL.
Scénarios¶
Le registre implémente les sept IDs contractuels :
S0 current baseline
S1 large feed
S2 high churn price/stock
S3 concurrent merchants
S4 worker scaling 1/2/4 lorsque le Runtime l'autorise
S5 projection rebuild
S6 frontend traffic during heavy sync
La présence de S4 ne constitue pas une autorisation d'augmenter la concurrence live. Le contrat Runtime CURRENT reste prioritaire.
Autorité de mutation¶
Le harness n'a aucune autorité pour :
systemctl start|stop|restart|enable|disable
docker compose up|down|restart
Toute mutation future de P1.5G appartient au workload explicitement fourni et doit avoir sa propre preuve de recovery.
État P1.5E¶
benchmark contract GIT CONFIGURED
benchmark runner GIT CONFIGURED
CI architecture guard GIT CONFIGURED
plan-mode smoke GIT CONFIGURED
live benchmark NOT EXECUTED
VPS capacity NOT CERTIFIED
P1.5E certifie le mécanisme de mesure, pas le résultat de capacité.
La prochaine phase est P1.5F : baseline production read-only avec le même contrat d'artefacts.
P1.5F dispose également d'un certifier opérateur unique :
scripts/capacity-certify-p1-5f-s0.sh
Il chaîne le dataset preflight, le plan obligatoire, exactement 1 warm-up + 3 runs mesurés, puis valide les manifests et la stabilité des sept containers sur toute la fenêtre. Il reprend automatiquement les autorités dataset depuis preflight.env, refuse un SHA/worktree divergent et conserve toutes ses preuves hors dépôt. Le mode --plan-only ne ferme jamais la phase. Une exécution complète réussie produit LIVE_EVIDENCE_PASS_PENDING_GIT_CLOSURE : la preuve live doit encore être revue et enregistrée par un lot Git avant P1.5F=CLOSED_PASS.
P1.5G définit le contrat fail-closed des scénarios S1-S6 dans benchmarks/capacity/p1-5g-controlled-live-contract.json. P1.5F est CLOSED_PASS; S1 large_feed, S2 high_churn_price_stock et S5 projection_rebuild sont CLOSED_PASS avec preuves live Git-recorded respectivement dans benchmarks/capacity/p1-5g-s1-live-closure.json, benchmarks/capacity/p1-5g-s2-live-closure.json et benchmarks/capacity/p1-5g-s5-live-closure.json. S3 et S6 restent requis pour la clôture globale ; S6 reste en plus bloqué avant préparation sur son workload-source authority. S4 reste SKIPPED_NOT_AUTHORIZED sans autorité explicite de concurrence. Le harness conserve son absence d'autorité de mutation.
P1.5H-prep définit le contrat de certificat final dans benchmarks/capacity/p1-5h-certificate-contract.json. Le template reste NOT_CERTIFIED tant que P1.5F/P1.5G ne fournissent pas de preuves live compatibles. Le certificat est obligatoirement scopé à l'enveloppe observée, conserve médiane + pire run, identifie la ressource limitante et sa marge, interdit toute extrapolation et ne valide un design target qu'avec sa provenance mesurée.
Benchmark reproductible¶
Chaque benchmark P1.5 devra figer au minimum :
git_sha
worker_image_digest
compose/config hashes
dataset_id
dataset_sha256
offers
products
merchant_count
change_percent
worker_count
scenario_id
benchmark_harness_version
started_at
finished_at
Puis conserver :
business correctness result
throughput metrics
CPU/RAM/IO peaks
DB latency
queue max + drain time
projection lag
frontend p95/p99
HTTP 5xx ratio
errors/restarts/OOM
Répétitions¶
Pour une certification :
1 warm-up
3 measured runs minimum
Conserver au minimum la médiane et le pire run mesuré.
Scénarios de certification¶
Le futur harness doit couvrir :
S0 current baseline
S1 large feed
S2 high churn price/stock
S3 concurrent merchants
S4 1/2/4 workers when current architecture safely permits it
S5 projection rebuild
S6 frontend traffic during heavy sync
Un scénario 2/4 workers n'est exécuté que si le contrat Runtime courant permet cette concurrence sans bricolage live.
PASS / FAIL¶
Hard FAIL¶
Un scénario est FAIL indépendamment du throughput en cas de :
- résultat métier différent ;
- perte de données ;
- projection incohérente ;
- OOM ;
- restart inattendu ;
- deadlock non récupéré ;
- queue qui ne se draine pas ;
- job abandonné silencieusement ;
- erreur 5xx au-delà du SLO ;
- saturation disque provoquant un échec fonctionnel.
Cibles initiales à mesurer¶
Les cibles de conception déjà présentes dans performance.md restent :
| Indicateur | Cible initiale |
|---|---|
| HTTP 5xx | < 0,1 % |
| frontend p95 | < 300 ms |
| projection lag normal | < 5 min |
Ces valeurs ne sont pas déclarées certifiées par P1.5A.
Safety margin¶
Le certificat final doit identifier le goulot limitant et sa marge.
Une configuration qui ne respecte les SLO qu'à saturation quasi permanente n'est pas considérée comme ayant une marge industrielle.
P1.5A ne fixe pas un pourcentage universel de marge : P1.5H devra documenter la marge observée par ressource et le premier seuil limitant.
Ordre des lots¶
P1.5A Capacity Measurement Contract
P1.5B1 Sync runtime capacity events
P1.5B2 Projection product-grain events + projection refresh job success/error
P1.5B3 Sync batch failure events + semantic gap closure
P1.5B4A Semantic boundary + committed offer-replay change events
P1.5B4B1 Normal-sync canonical offer semantic diff
P1.5B4B2A1 Taxonomy V2 direct canonical offer writers
P1.5B4B2A2 Governance direct canonical offer writers
P1.5B4B2B1 Terminal product-grain identity model + durable state
P1.5B4B2B2A Durable identity capacity outbox
P1.5B4B2B2B Runtime product-grain identity orchestration
P1.5B4B2B2C Durable identity capacity delivery
P1.5C1A Alloy log authority / Promtail migration
P1.5C1B Runtime event metrics → Alloy /metrics → Prometheus
P1.5C1C Read-only capacity gauges → Alloy /metrics → Prometheus
P1.5C2B MariaDB histogram + row-write counter → node-exporter textfile
P1.5D Capacity & SLO dashboard/recording rules
P1.5E Reproducible benchmark harness
P1.5F Read-only production baseline
P1.5G Controlled live benchmark — S1/S2/S5 CLOSED_PASS; S3/S6 still required; S4 SKIPPED_NOT_AUTHORIZED
P1.5H Capacity certificate and SLO closure — Git template prepared, live evidence required
P1.5A est Git-only.
Les premières mutations VPS ne sont justifiées qu'après certification Git/off-live des lots qui doivent être déployés.
Critères de sortie P1.5A¶
P1.5A est fermé lorsque :
- le registre machine-readable contient toutes les métriques obligatoires ;
- noms, types, unités et labels sont explicites ;
- cardinalité haute interdite ;
- sources CURRENT déjà disponibles distinguées des métriques manquantes ;
- aucune série MariaDB inexistante n'est prétendue disponible ;
- les formules p95/p99/5xx/throughput sont explicites ;
- queue depth/lag ont une définition canonique ;
- projection lag ne repose pas sur une approximation temporelle invalide ;
- le contrat est gardé automatiquement en CI ;
- aucune configuration Runtime n'a changé.