Files
khadhroony-solana-project/docs/plans/039-V0_3_17_RAW_OPERATIONAL_RESILIENCE_PLAN.md
2026-09-24 10:54:04 +02:00

39 KiB

Plan 0.3.17 — résilience opérationnelle et cycle de vie complet des variantes RAW

1. Objet

Ce document ferme le gate d'ouverture 0.3.17-pre.001 demandé par prompts/036-V0_3_17_START_PROMPT.md.

La release part exclusivement de la stable v0.3.16 et doit compléter les fondations multi-variantes sans développer encore la Store Desk.

La cible est volontairement backend/runtime :

Store API
  inspection variantes/conflits/historique
  actions revisionnées
  classification Store Transient / Terminal

PostgreSQL
  conflict cases complets
  participants
  historique append-only
  résolution/reopen concurrent-safe
  rétention/purge guards des variantes

Store facade
  dispatch backend-neutral
  classification publique sûre

Worker
  retry Store borné
  backpressure
  activity Blocked
  cancellation/drain

Transport/Config
  durcissement des reconnects propriétaires existants
  aucune nouvelle pile réseau
  reconnect/replay/coverage toujours distincts

0.3.18 consommera ensuite ces contrats dans ksp-app-store-desk.

2. Base stable et audit d'ouverture

Baseline logique requise :

v0.3.16
workspace.package.version = 0.3.16

L'archive reçue contient bien :

prompts/036-V0_3_17_START_PROMPT.md
prompts/035-V0_3_16_START_PROMPT.md
docs/plans/037-V0_3_16_RAW_RESILIENCE_CONFLICT_HANDOFF.md
docs/plans/038-V0_3_16_RAW_RESILIENCE_CONFLICT_PLAN.md
docs/validation/033-V0_3_16_RAW_RESILIENCE_CONFLICT.md
deltas/0.3.16/pre.001.md ... pre.010.md
deltas/0.3.16/pre.*-fix.* réellement publiés
deltas/0.3.16/rel.001.md

Le workspace comporte 22 membres et tous les manifests membres héritent de workspace.package.version et de l'édition workspace.

Les audits statiques du dépôt exécutés sur l'archive avant modification sont propres :

General Rust rule audit: clean
Rust export completeness audit: 0 candidate(s)
KSP workspace Rust rule audit: clean
Markdown table audit: clean (352 table(s), 962 file(s))

Le toolchain Cargo n'est pas disponible dans l'environnement d'assemblage de cette tranche. Les commandes Cargo ne sont donc pas déclarées réexécutées ici. Le gate utilisateur de la stable 0.3.16 fourni avec l'archive reste la preuve externe de fmt/check/clippy et des suites ciblées de fermeture.

L'archive binaire a été matérialisée localement et son SHA-256 est enregistré dans docs/validation/034-V0_3_17_RAW_OPERATIONAL_RESILIENCE.md.

L'environnement d'assemblage n'a pas permis une comparaison réseau indépendante du commit du tag Gitea. La provenance « ZIP téléchargé depuis le tag v0.3.16 » reste donc fournie par l'opérateur ; l'identité stable interne de l'arbre est cohérente avec cette provenance.

3. Règles normatives réauditées

Les sources suivantes ont été relues dans l'ordre imposé par le prompt :

RULES.md
ROADMAP.md
CHANGELOG.md
docs/000-README.md

docs/rules/RULES_GENERAL.md
docs/rules/RULES_KSP.md
docs/rules/RULES_RUST.md
docs/rules/RULES_DEPENDENCIES.md
docs/rules/RULES_DOCUMENTATION.md
docs/rules/FILE_CONTRACTS.md
docs/rules/VERSION_WORKFLOW.md
docs/rules/PROMPT_STRUCTURE.md

Conséquences bloquantes pour 0.3.17 :

  • Rust 2024 reste la seule édition workspace ;
  • unsafe reste interdit ;
  • unwrap, expect, panic et retours implicites restent interdits selon les lints KSP ;
  • aucun pub mod n'est ajouté ;
  • tout item partagé pub/pub(crate) est réexporté au crate root et consommé via crate::Item intra-crate ;
  • les tests unitaires restent sous unit_tests/, les tests d'intégration sous tests/ ;
  • V000/V001/V002/V003 sont immuables une fois publiés ; toute évolution physique devient une migration additive suivante ;
  • le Store physique reste privé derrière ksp-store-lib ;
  • le Worker ne crée ni backend Store direct, ni client PostgreSQL, ni seconde pile Transport ;
  • le Job Backfill et le Worker restent des producteurs indépendants ;
  • aucune queue ou retry non borné n'est autorisé ;
  • un PASS live n'est déclaré que lorsqu'il a réellement été exécuté ;
  • pre.001 est un gate de brainstorming/planification et ne doit pas contenir d'implémentation Rust/SQL lourde ;
  • la lane technique/live finale, la réconciliation documentaire et la préparation de publication restent trois étapes distinctes ;
  • la première prerelease 0.3.17-pre.001 utilise la version Cargo 0.3.17-pre.1.

4. Baseline fonctionnelle héritée de 0.3.16

4.1 Store API

La stable fournit déjà :

RawTransactionVariantId
RawTransactionVariantReference
RawTransactionVariantOrigin
RawTransactionVariantRelation
RawTransactionVariantRelationReason
RawTransactionVariantComparison
RawTransactionVariantWriteOutcome
RawAcquisitionWriteOutcome::transaction_variant()
RawTransactionConflictStatus { Open, Resolved }

Le comparateur partagé reste l'autorité automatique et conserve exactement les relations :

Exact
CompatibleLessComplete
CompatibleMoreComplete
Conflict
Incomparable

La seule dominance actuellement prouvée est la troncature stricte de logMessages. Aucun élargissement de dominance n'est prévu dans 0.3.17.

Les capabilities actuelles couvrent canonical transaction, observations, inspection canonical/observations et rétention V001. Elles ne fournissent pas encore de capability publique dédiée aux variantes, conflict cases, historique ou actions de résolution.

4.2 PostgreSQL V003

V003 possède quatre objets métier centraux :

ksp_raw_transaction_variants
ksp_raw_transaction_canonical_selectors
ksp_raw_transaction_observation_variants
ksp_raw_transaction_conflicts

Le conflict case V003 est volontairement minimal : une ligne courante par signature, avec :

status
revision
canonical_variant_id
incoming_variant_id
latest_relation
latest_reason_code
created_at
updated_at

Il ne contient ni participant ledger complet, ni journal append-only, ni résolution explicite vers une variante, ni historique d'actions.

4.3 Promotion et conflit actuels

L'acquisition PostgreSQL sérialise d'abord l'identité canonical V001 avec FOR UPDATE, bootstrap le ledger V003, compare les bytes et :

Exact                         -> réutilise le canonique
CompatibleLessComplete       -> conserve la variante reçue, canonique inchangé
CompatibleMoreComplete       -> promotion atomique selector + projection V001
Conflict / Incomparable      -> conserve la variante, ouvre/met à jour le conflict case

Une divergence durable Conflict/Incomparable retourne QuarantinedConflict, puis l'observation est liée à la variante réellement reçue avant commit.

4.4 Worker actuel

Le Worker dépend déjà de ksp-store-lib, pas du backend PostgreSQL.

La convergence run-local ne fait que sérialiser une même identité. Chaque acquisition atteint ensuite le Store.

QuarantinedConflict est un succès durable ; le Worker reste Running et projette une health Degraded.

Une autre erreur Store est aujourd'hui projetée comme erreur de persistence et devient terminale. Il n'existe pas encore de retry Store dédié.

WorkerActivity contient actuellement :

Unknown
Idle
Active

WorkerHealth contient :

Unknown
Healthy
Degraded
Unhealthy

La notion Blocked n'est donc pas encore matérialisée.

4.5 Store error surface actuelle

ksp-store-lib expose des codes sûrs mais plusieurs sont backend-spécifiques :

postgres_connect_failed
postgres_pool_timeout
postgres_read_failed
postgres_write_failed
postgres_data_invalid
postgres_migration_failed
postgres_migration_mismatch
postgres_schema_newer
postgres_tls_failed
wrong_network
...

Le backend PostgreSQL redacted les erreurs physiques en PostgresBackendErrorKind, mais les échecs tokio-postgres de statement/transaction sont encore souvent aplatis en ReadFailed ou WriteFailed sans conserver le SQLSTATE structuré.

Cette perte d'information doit être corrigée avant de prétendre classifier sûrement Transient/Terminal.

4.6 Transport/Config actuels

WebSocket et Yellowstone possèdent déjà chacun un reconnect Transport-owned :

max_retries
initial_backoff
max_backoff

Config sait déjà projeter les defaults et overrides vers ces settings.

Yellowstone possède déjà une borne KSP MAX_GRPC_RECONNECT_RETRIES = 100, ainsi que connect/unary/close timeouts bornés.

Le WebSocket valide les backoffs et leur ordre mais ne borne pas encore explicitement max_retries à un maximum KSP. Cette asymétrie est une cible de hardening 0.3.17, pas la justification d'une nouvelle boucle de reconnect.

Yellowstone conserve déjà des projections distinctes pour :

reconnect
requested from_slot
replay delivery
replay coverage unproven
SubscribeReplayInfo first_available

Les contrats gap/coverage de 0.3.14 restent autoritaires.

5. Réaudit upstream utile à pre.001

5.1 PostgreSQL

PostgreSQL 17 documente que les row locks sont tenus jusqu'à la fin de transaction, que les deadlocks sont détectés avec abandon d'une transaction, et recommande un ordre de locks cohérent. Il documente également le retry des serialization_failure SQLSTATE 40001 et, lorsque pertinent, des deadlock_detected SQLSTATE 40P01.

Politique KSP retenue :

ordre de locks déterministe
transactions courtes
aucune attente opérateur sous transaction
40001 -> Transient
40P01 -> Transient
23505/23P01 -> pas de retry générique ; classifier selon l'invariant KSP concret

5.2 tokio-postgres / deadpool-postgres

tokio-postgres expose un DbError::code() structuré de type SqlState. Deadpool distingue notamment Timeout, Backend, Closed, NoRuntimeSpecified et PostCreateHook.

Politique KSP retenue :

inspecter type/SQLSTATE avant redaction
ne jamais classifier par texte serveur
ne jamais conserver le message DB arbitraire dans l'erreur publique
projeter uniquement classe publique + code KSP sûr + phase statique

5.3 Solana WebSocket

Les subscriptions Solana fournissent des notifications de session ; signatureSubscribe est explicitement one-shot et supprimé par le serveur après notification. Les docs PubSub ne fournissent pas une garantie générale de replay/coverage après remplacement d'une session.

Politique KSP retenue :

reconnect physique -> fait Transport
resubscribe -> restauration logique de subscription
aucune de ces étapes ne prouve la coverage
la réparation/gap ledger Worker reste séparée

5.4 Yellowstone

Le proto upstream expose SubscribeRequest.from_slot; KSP possède déjà SubscribeReplayInfo et une projection conservatrice du replay.

Politique KSP retenue :

from_slot = demande de replay
ReplayInfo = borne de disponibilité annoncée
replay delivery = preuve de réception de matériau
coverage = preuve distincte et plus forte

Aucun reconnect réussi n'efface un gap par lui-même.

5.5 Tokio

Le runtime de retry doit être cancellable. tokio::select! annule les branches non retenues ; les primitives à file d'attente équitable comme Mutex::lock et Semaphore::acquire demandent une attention particulière à la cancellation safety.

Politique KSP retenue : le backoff Store utilise un sleep stop-preemptible et ne réorganise pas silencieusement l'admission ou les permits de persistence.

6. Modèle conceptuel conflict case retenu

6.1 Identité

Un conflict case est durablement identifié par la même identité transactionnelle backend-neutral :

RawTransactionConflictReference
    transaction: RawTransactionReference

Aucun UUID ou identifiant provider n'est nécessaire.

Le backend PostgreSQL reste lié à un seul network, mais la DTO publique conserve le RawTransactionReference complet afin que l'identité publique reste (network, signature).

6.2 Etats

Aucun troisième état n'est justifié au gate :

Open
Resolved

Les notions « stale », « blocked », « archived » ou « unavailable payload » sont des outcomes/états d'autres objets et ne deviennent pas des statuts artificiels du conflict case.

6.3 Revision

Chaque mutation durable du conflict case incrémente une revision u64 strictement monotone :

création Open                  revision = 1
nouveau participant/relation  revision + 1
résolution                     revision + 1
reopen                         revision + 1
nouvelle divergence après Resolved -> reopen atomique + revision + 1

Une réobservation strictement identique d'un état Open déjà représenté reste idempotente et ne consomme pas une revision supplémentaire.

L'épuisement u64 est terminal/fail-closed ; aucun wrap n'est autorisé.

6.4 Participants

Le participant ledger est monotone par conflict case :

(transaction, variant_id) unique
first_seen_revision
first_seen_at

Le canonique impliqué dans la divergence et chaque variante divergente observée deviennent participants.

Un participant n'est pas supprimé lors de la résolution ou d'un reopen ; le journal conserve l'audit de la case complète.

6.5 Historique append-only

Chaque transition réelle produit un événement append-only :

Opened
ParticipantAdded
ResolvedKeepCanonical
ResolvedPromoteVariant
ResolvedRestoreVariant
Reopened

Un événement contient seulement les références/codes nécessaires :

conflict reference
conflict revision
event kind
canonical before
candidate/participant lorsque pertinent
canonical after
relation/reason lorsque pertinent
resolution action lorsque pertinent
timestamp KSP borné

Aucun payload RAW, SQL, texte serveur ou chaîne acteur libre n'entre dans l'historique.

0.3.17 ne possède pas de système d'identité/authentification opérateur. Il ne doit donc pas inventer un actor arbitraire. Le journal distingue seulement l'origine sémantique sûre de l'action lorsqu'elle est utile :

Automatic
Operator

L'identité humaine authentifiée éventuelle appartient à une couche future disposant réellement de ce contrat.

6.6 Résolution courante

Le conflict case courant expose :

status
revision
canonical variant courante
selector revision courante
latest incoming participant
latest relation/reason
resolved canonical variant si Resolved
latest resolution action si Resolved
created_at
updated_at
participant_count

Le couple conflict_revision + canonical_revision évite de confondre évolution du conflit et évolution du selector.

7. Contrats Store API proposés

Les noms exacts peuvent être ajustés lors de pre.002 pour respecter les inventaires publics, mais la sémantique suivante est fixée.

7.1 Inspection des variantes

DTOs backend-neutral :

RawTransactionVariantSummary
RawTransactionVariantInspectionQuery
RawTransactionVariantDetail

Le summary reste payload-free et expose au maximum :

variant reference
origin
slot
block_time
format identity
retention state
payload size si disponible
is_current_canonical
observation count
created_at

Le Debug ne rend pas la signature brute, les bytes ou un hash de contenu sensible lorsque les règles existantes l'interdisent.

Le detail explicite peut fournir la transaction complète uniquement via une capability dédiée et bornée ; les listes interactives restent payload-free.

Capability cible :

RawTransactionVariantInspectionRead

7.2 Inspection des conflict cases

DTOs :

RawTransactionConflictReference
RawTransactionConflictSummary
RawTransactionConflictInspectionQuery
RawTransactionConflictDetail
RawTransactionConflictParticipantSummary

Capability cible :

RawTransactionConflictInspectionRead

La liste est random-access bornée comme les inspections Store actuelles. La query peut filtrer au minimum par network/status et, lorsque nécessaire, par transaction exacte ; elle n'invente pas de recherche texte libre.

7.3 Historique

DTOs :

RawTransactionConflictEventKind
RawTransactionConflictEventSummary
RawTransactionConflictHistoryQuery

Capability cible :

RawTransactionConflictHistoryRead

L'historique est ordonné par revision et paginé ; aucun event n'embarque de payload RAW.

7.4 Actions mutables

Une seule capability cohérente peut porter des requests typées :

RawTransactionConflictActionWrite

Actions :

KeepCurrentCanonical
PromoteVariant
RestoreVariant
Resolve
Reopen

Resolve n'est pas une fusion implicite ; il clôt un case avec une décision canonique explicite. PromoteVariant et RestoreVariant sélectionnent une variante durable donnée. Le nom Restore signale l'intention opérateur mais utilise les mêmes garanties atomiques de selector que Promote.

Chaque request mutable contient :

conflict reference
expected_conflict_revision
expected_canonical_revision
cible variant lorsque pertinent

Les revisions valent u64 > 0.

7.5 Outcomes d'action

Les outcomes ne transforment pas une concurrence métier attendue en erreur opaque :

Applied
AlreadyAtTarget
StaleRevision
ConflictNotOpen
ConflictNotResolved
VariantNotParticipant
VariantPayloadUnavailable

La liste exacte sera minimisée en pre.002, mais StaleRevision et AlreadyAtTarget doivent rester distincts.

Idempotence retenue : une répétition d'une action dont la cible est déjà exactement l'état durable demandé renvoie AlreadyAtTarget sans nouvelle revision. Une requête stale visant un autre état renvoie StaleRevision et ne modifie rien.

8. Classification publique Store Transient / Terminal

8.1 Type public

ksp-store-api doit posséder un enum backend-neutral non exhaustif :

StoreErrorClass
  Transient
  Terminal

La classification d'une erreur concrète doit être obtenue via un contrat stable, pas par comparaison ad hoc de strings dans le Worker.

Contrat cible :

StoreErrorClassifier
  classify_store_error(&Error) -> StoreErrorClass

ksp-store-lib::Store implémente ce contrat. Le Worker enrichit son port privé de persistence avec cette capability au lieu de connaître les codes PostgreSQL.

Fail-closed : un code inconnu est Terminal jusqu'à classification explicite.

8.2 Matrice KSP retenue

Transient :

connexion physique indisponible/reset après configuration valide
pool temporairement indisponible / timeout d'acquisition
SQLSTATE classe connection exception lorsque l'opération est rejouable
40001 serialization_failure
40P01 deadlock_detected
57P01 admin_shutdown
57P02 crash_shutdown
57P03 cannot_connect_now

Terminal :

configuration/secret/TLS local invalide
backend non compilé
network KSP mismatch
modèle/query invalide
stored data incompatible
migration mismatch
schema newer
constraint révélant une violation d'invariant KSP
legacy raw content conflict
stale revision
retention/pin conflict
variant absent ou payload indisponible pour action locale
page limit non représentable
retention compaction unsupported

Cas à ne pas généraliser :

23505 unique_violation
23P01 exclusion_violation

Ils ne deviennent pas Transient par défaut. Une opération KSP spécifique peut prouver qu'un de ces SQLSTATE représente une race retryable ; sinon fail-closed Terminal.

8.3 Cancellation

Une cancellation demandée localement par le Worker n'est pas un outage Store à retryer. Le runtime traite d'abord son token de stop/cancellation ; une erreur résultante n'est jamais recyclée en boucle de retry.

8.4 Refactor PostgreSQL requis

Le backend ne doit plus convertir immédiatement toute erreur de statement en ReadFailed/WriteFailed avant inspection.

Une fonction privée unique doit :

inspecter tokio_postgres::Error
extraire DbError::code() si présent
classifier les SQLSTATE explicitement supportés
classifier les erreurs connection/closed/IO de façon sûre
jeter le texte/source externe après projection
retourner PostgresBackendErrorKind + phase statique

Les détails SQLSTATE restent privés ; la frontière publique ne reçoit que le code KSP redacted et StoreErrorClass.

9. Stratégie PostgreSQL V004

9.1 Principe

V000/V001/V002/V003 restent byte/checksum-identiques.

0.3.17 crée une migration additive V004 uniquement après stabilisation des contrats pre.002.

9.2 Objets requis

La forme physique minimale envisagée :

extension additive de ksp_raw_transaction_conflicts
  resolved_canonical_variant_id nullable
  latest_resolution_action nullable
  resolved_at_unix_millis nullable

ksp_raw_transaction_conflict_participants
  transaction_signature
  variant_id
  first_seen_revision
  first_seen_at_unix_millis

ksp_raw_transaction_conflict_events
  transaction_signature
  revision
  event_kind
  canonical_before_variant_id
  candidate_variant_id nullable
  canonical_after_variant_id
  relation nullable
  reason_code nullable
  resolution_action nullable
  action_origin
  created_at_unix_millis

Les colonnes exactes sont validées dans pre.003 avec les contraintes réelles ; aucun champ libre n'est ajouté « au cas où ».

9.3 Bootstrap legacy V003

Pour un conflict case déjà présent avant V004 :

conserver status/revision V003
créer les participants prouvables depuis canonical_variant_id + incoming_variant_id
créer un événement BaselineImported ou équivalent explicitement qualifié legacy
ne pas inventer les transitions antérieures perdues
ne pas prétendre reconstruire un ancien canonical non prouvé

Si BaselineImported n'apporte pas assez de valeur, pre.003 peut retenir un marqueur history_complete_from_revision sur la projection. Il est interdit de fabriquer un historique détaillé.

10. Ordre de locks et CAS

Ordre global retenu pour toute mutation d'une transaction RAW concernée :

1. ksp_raw_transactions identité V001        FOR UPDATE
2. canonical selector                         FOR UPDATE / CAS
3. conflict current row                       FOR UPDATE / CAS
4. variants nécessaires                       ordre variant_id croissant si plusieurs locks sont requis
5. participant/event/pin sidecars             inserts/updates déterministes
6. projection V001 + selector + conflict      même transaction
7. commit

Aucune interaction UI, sleep, réseau ou retry n'a lieu à l'intérieur de cette transaction.

La résolution manuelle doit relire et comparer :

expected_conflict_revision
expected_canonical_revision

avant toute mutation.

La promotion automatique CompatibleMoreComplete doit utiliser le même ordre de locks et continuer de ne s'appliquer qu'à une dominance prouvée.

11. Matrice de races

Ingestion pendant résolution

La première transaction qui détient l'identité V001 décide. La seconde relit selector/conflict après acquisition du lock et réévalue sa request/comparaison. Aucun résultat calculé avant lock n'est appliqué aveuglément.

Promotion automatique pendant résolution manuelle

Même lock order. Si la promotion automatique gagne, la canonical_revision change et la résolution manuelle devient stale sauf si sa cible est déjà exactement atteinte.

Deux résolutions concurrentes

Une seule peut satisfaire les deux expected revisions. L'autre retourne StaleRevision, sauf répétition exacte de la cible déjà durable -> AlreadyAtTarget.

Nouvelle divergence après Resolved

L'ingestion ouvre de nouveau la même conflict identity, incrémente la conflict revision, ajoute le participant si nouveau et écrit Reopened/ParticipantAdded de manière append-only. La décision précédente reste historique.

Rétention pendant résolution

La rétention acquiert d'abord l'identité V001 selon le même ordre et réévalue les guards sous lock. Elle ne peut purger les bytes d'une variante requise par le selector courant ou un conflict case Open.

ForceRehydrate

Le contrat V001 reste : la tombstone purgée conserve assez d'identité/hash pour refuser une rehydratation incompatible. ForceRehydrate ne choisit jamais automatiquement une autre variante et ne devient pas un mécanisme de rollback réseau.

12. Rétention des variantes

12.1 Deux notions distinctes

Le plan distingue :

identity retention
payload retention

L'identité d'une variante référencée par selector, observation mapping ou historique n'est jamais supprimée.

Les bytes peuvent suivre :

Full -> Archived -> Purged

uniquement lorsque les guards l'autorisent.

12.2 Hard payload pins

Purge interdite lorsque la variante est :

canonical selector courant
participant d'un conflict case Open
cible d'une mutation en cours sous transaction

Une observation mapping et l'historique conservent l'identité mais ne pin pas éternellement le payload à eux seuls.

Cette décision évite qu'un historique append-only transforme toute divergence passée en rétention infinie des bytes.

12.3 Resolution et rollback

Après résolution :

la variante canonique reste pin par le selector
les variantes perdantes peuvent être archivées
une purge reste une action explicite de rétention, jamais automatique dans Store

RestoreVariant fonctionne uniquement avec une variante dont les bytes sont encore localement disponibles Full ou Archived.

Si le payload a été explicitement purgé :

VariantPayloadUnavailable

est retourné ; le Store ne consulte pas le réseau et ne fabrique pas les bytes.

L'historique continue de pointer vers l'identité de la variante purgée et rend donc la perte de capacité de rollback visible, non silencieuse.

12.4 Archive physique des variantes

V003 sait représenter retention_state sur les variantes mais ne possède pas encore le sidecar d'archive équivalent au canonical V001.

pre.005 doit décider/implémenter l'objet physique minimal permettant :

Full variant payload inline
Archived variant payload local sidecar
Purged variant payload absent mais metadata/identity conservées

Aucune suppression de row de variante n'est nécessaire.

13. Worker Store retry

13.1 Ownership

Le backend classe et retourne.

Le Worker décide :

retry count
backoff
stop preemption
backpressure
health/activity
exhaustion

Aucune boucle retry cachée n'est ajoutée dans PostgreSQL ou ksp-store-lib.

13.2 Settings ciblés

Extension de RawTransactionIngestSettings :

store_retry_max_retries
store_retry_initial_backoff
store_retry_max_backoff

Valeurs proposées :

default max_retries       = 5
maximum max_retries       = 20
default initial_backoff   = 250 ms
default max_backoff       = 5 s
minimum backoff           = 10 ms
maximum backoff setting   = 30 s

max_retries = 0 désactive le retry et conserve un comportement terminal immédiat sur une erreur Transient.

Aucun jitter n'est introduit dans la première implémentation. L'ownership reste explicitement Worker et peut être réaudité plus tard si les mesures live montrent un herd effect réel. La concurrence de persistence et l'admission bornée constituent déjà la première limite anti-storm.

13.3 Backoff

Backoff exponentiel borné :

initial * 2^(attempt - 1)
cap = max_backoff

Le calcul est overflow-safe et saturé au cap.

Le sleep est stop-preemptible avec tokio::select!.

13.4 Aucune retry queue supplémentaire

Une acquisition admise conserve son slot de travail pendant toute sa séquence de retry.

Il n'existe pas de queue de retry distincte :

source -> admission queue bornée -> persistence concurrency permit -> attempt/backoff/attempt -> durable outcome ou exhaustion

Ainsi :

  • le nombre de persistence sequences actives reste borné par persistence_concurrency ;
  • lorsque ces séquences sont bloquées sur Store, l'admission queue se remplit puis applique naturellement la backpressure existante ;
  • aucune croissance mémoire sans borne n'est possible.

13.5 Ambiguous commit

Un échec de connexion après envoi peut être ambigu quant au commit serveur.

Le retry est autorisé uniquement parce que l'acquisition RAW Store est conçue idempotente : identité canonical, variante exacte et observation key déterministe convergent vers le même état durable.

Le Worker ne mémorise jamais « failed donc absent ».

Après retry, un résultat durable AlreadyPresent/AlreadyPresent est un succès normal.

13.6 Blocked / health

Blocked est une activité, pas une health.

Evolution prévue de ksp-worker-api::WorkerActivity :

Unknown
Idle
Active
Blocked

Projection :

retry Store en attente/backoff avec travail durable non terminé
  state    = Running
  activity = Blocked
  health   = Degraded

Store récupéré et pipeline reprend
  state    = Running
  activity = Active/Idle selon travail
  health   = politique normale, sauf autre dégradation

retry exhausté / erreur Terminal
  state    = Faulted(code)
  health   = Unhealthy

Le snapshot RAW ajoute des compteurs bornés/checked nécessaires, sans code PostgreSQL physique :

store_retry_total
store_transient_failure_total
store_retry_exhaustion_total
store_blocked_in_flight

Les noms définitifs seront minimisés en pre.007.

13.7 Stop et drain

Pendant backoff : stop interrompt immédiatement le sleep et passe au drain normal.

Une persistence déjà envoyée au Store n'est pas relancée aveuglément en parallèle. La séquence attend son résultat ou le mécanisme de drain/abort existant, puis aucune nouvelle tentative n'est admise après la décision de stop.

14. Transport/Config 0.3.17

14.1 Pas de nouvelle abstraction universelle

Les acteurs existants restent propriétaires :

WsSession -> reconnexion WebSocket
Yellowstone subscribe stream -> reconnexion/replay gRPC
HTTP runtime -> retry HTTP existant

Le Worker ne reçoit aucun socket ou channel bas niveau.

14.2 WebSocket

Hardening prévu :

ajouter une borne KSP explicite à reconnect.max_retries
aligner les validations Config/Transport
vérifier overflow-safe du backoff
conserver stop preemption et resubscribe existants
conserver remote subscription IDs privés

Aucun changement de semantics de coverage.

14.3 Yellowstone

Le gRPC possède déjà les primitives requises. 0.3.17 doit principalement vérifier :

bornes Config identiques aux bornes Transport
connect/session deadlines réellement appliquées
close/stop toujours bornés
reconnect attempt snapshot cohérent
from_slot/ReplayInfo conservateurs
aucune fermeture de gap par simple reconnect

14.4 Reset d'un budget de reconnect

Le comportement existant reset le budget après un reconnect réussi. Le gate technique doit vérifier qu'un flapping durable reste observable et ne produit pas une boucle CPU : chaque perte de session réapplique le backoff borné et les compteurs de lifecycle restent visibles.

Aucun reset_after_stable_duration supplémentaire n'est justifié dans cette release sans défaut concret.

15. Compatibilité Job Backfill

Le Job Backfill reste inchangé dans son ownership :

Transport observé -> canonicalisation RAW commune -> Store facade

Il doit continuer à :

  • traiter QuarantinedConflict selon son contrat actuel ;
  • ne pas dépendre des nouvelles capacités opérateur pour persister ;
  • ne pas implémenter le retry live Worker ;
  • ne pas recevoir de dépendance Worker ;
  • rester compilable lorsque les traits Store existants sont étendus par de nouveaux traits séparés plutôt que par modification incompatible des traits déjà implémentés.

C'est une raison supplémentaire pour ajouter des capabilities fines séparées au lieu d'ajouter des méthodes obligatoires à RawTransactionWrite.

16. Sécurité et redaction

Les nouvelles surfaces ne doivent jamais exposer :

PostgreSQL URI/password
SQL statement
DbError message/detail/hint
endpoint complet avec secret
payload RAW dans summary/history/error
signature brute dans Debug lorsque la politique actuelle la redacted
content hash dans Debug si la surface existante l'interdit
backend row/client/transaction handle

Les SQLSTATE sont utilisés en interne pour classification, pas comme dépendance publique du Worker.

Les reason/action codes publics sont des enums/codes KSP bornés, jamais des chaînes serveur ou saisies utilisateur libres.

17. Tests et preuves nécessaires

17.1 Store API

Canaris attendus :

constructibilité crate-root
object safety des nouveaux traits
non-exhaustive des enums évolutifs
validation des revisions non nulles
queries/histories bornées
Debug payload-free/redacted
external backend peut implémenter les nouveaux contrats sans dépendance runtime
classification inconnue fail-closed Terminal

17.2 V004/PostgreSQL

Canaris :

V000/V001/V002/V003 bytes/checksums inchangés
registry V004 exacte
bootstrap legacy sans historique fabriqué
participants monotones
history append-only
revision CAS
selector/projection atomiques
ordre de locks stable
résolution concurrente
reopen concurrent
rollback complet sur erreur tardive
purge guard sous race
variant archive/purge shape exact

17.3 Worker

Canaris :

Transient -> retry borné
Terminal -> aucun retry
max_retries=0 -> faute immédiate
backoff cap/overflow exact
stop pendant sleep
stop pendant attempt
queue bornée sous outage prolongé
activity Blocked + health Degraded
recovery -> reprise sans respawn Worker
exhaustion -> Faulted/Unhealthy
ambiguous durable success -> idempotence, pas double observation
QuarantinedConflict reste succès non terminal

17.4 Transport

Canaris :

WS reconnect max_retries borné
WS/grpc backoff borné et stop-preemptible
Config projection exacte defaults/overrides
reconnect != replay delivery
replay delivery != coverage
Yellowstone from_slot/ReplayInfo non régressés

17.5 Live PostgreSQL

La lane technique finale doit exécuter un test opt-in PostgreSQL réel couvrant au minimum :

open conflict -> participants/history
résolution CAS
stale concurrent action
reopen par nouvelle divergence
restore d'une variante locale
purge guard d'un participant Open
archive puis restore lorsque supporté
transient-classification path observable sans secret
rollback transactionnel

La version serveur réellement observée est journalisée de manière sûre ; aucune URI n'est rendue.

17.6 Outage/recovery Store

Une preuve ciblée doit simuler ou provoquer de manière déterministe :

Store temporairement indisponible
Worker Running/Blocked/Degraded
admission bornée
recovery avant exhaustion
persistence idempotente
retour à activité normale

Si une panne PostgreSQL réelle contrôlée n'est pas raisonnablement automatisable, le test déterministe sur le port privé Worker prouve la politique et le live PostgreSQL prouve séparément la classification physique.

18. Découpage recalibré de 0.3.17

Le découpage initial pre.001 -> pre.009 reste trop dense autour du couple contrats/migration/backend. Il est subdivisé sans fusionner les lanes de fermeture.

0.3.17-pre.001

Gate présent : lecture, audit, upstream re-audit, modèle conceptuel, contrats proposés, error classification, retry/reconnect policy, sizing et plan. Aucun Rust/SQL lourd.

0.3.17-pre.002

Store API : DTOs/capabilities variantes/conflicts/history/actions, expected revisions, outcomes stale/idempotent et type/contrat StoreErrorClass backend-neutral.

0.3.17-pre.003

PostgreSQL V004 additive : participants, history, projection de résolution et bootstrap legacy, avec canaris byte/checksum V000-V003.

0.3.17-pre.004

PostgreSQL lifecycle : inspection, resolve/promote/keep/restore/reopen, CAS double revision, lock order, races ingestion/résolution et history append-only.

0.3.17-pre.005

Rétention des variantes : archive locale, payload states, purge guards, interaction selector/open conflict, restore local et ForceRehydrate non régressé.

0.3.17-pre.006

ksp-store-lib : dispatch des nouvelles capabilities, classification publique, raffinement SQLSTATE/backend privé, erreurs redacted et régression Job Backfill.

0.3.17-pre.007

Worker : settings Store retry, backoff borné, WorkerActivity::Blocked, backpressure, cancellation, exhaustion, recovery et snapshots/counters sûrs.

0.3.17-pre.008

Transport/Config : bornage WebSocket manquant, cohérence reconnect settings, hardening session/stop et non-régression Yellowstone replay/coverage.

0.3.17-pre.009

Gate technique/live final : races cross-layer, Store outage/recovery, retry, reconnect, PostgreSQL live, tests workspace pertinents et graphes de dépendances requis.

0.3.17-pre.010

Réconciliation documentaire finale uniquement : plan, validation, README/USAGE et architectures durables concernées. Aucun nouveau comportement runtime.

0.3.17-pre.011

Préparation de publication minimale : CHANGELOG.md, ROADMAP.md, prompt 0.3.18, version et delta.

0.3.17-rel.001

Publication stable mécanique après gates propres.

La prévision reste souple : un défaut local peut produire un fix.*; une tranche trop lourde peut être subdivisée, mais la lane technique/live doit rester avant la réconciliation documentaire et la publication prep.

19. Fichiers attendus par tranche

Le plan ne pré-approuve pas des fichiers non nécessaires, mais l'ownership attendu est :

pre.002  ksp-store-api
pre.003  ksp-store-postgres-lib migration/schema registry/resources
pre.004  ksp-store-postgres-lib runtime/SQL/tests
pre.005  ksp-store-api + ksp-store-postgres-lib retention variant
pre.006  ksp-store-lib + PostgreSQL classification + Backfill regression tests
pre.007  ksp-worker-api + ksp-worker-raw-transaction-ingest-lib
pre.008  ksp-onchain-transport-lib + ksp-config-lib
pre.009  tests/hardening/live only selon besoins prouvés
pre.010  documentation durable
pre.011  release metadata/prompt

Toute dépendance nouvelle doit être justifiée séparément avant ajout. Aucune nouvelle crate n'est prévue.

20. Hors périmètre confirmé

0.3.17 ne contient pas :

Store Desk frontend/backend Tauri
Backfill multi-route/multi-stratégie
Backfill Desk multi-route
RAW -> STRUCTURAL
STRUCTURAL persistence
DECODED / DOMAIN
majority voting provider
provider priority
arbitrage canonique par provenance
fusion heuristique JSON
retry Store illimité
retry caché dans le backend
nouveau client PostgreSQL dans Worker
nouvelle pile WebSocket/gRPC dans Worker
nouvelle dépendance Worker <-> Backfill
reconstruction réseau comme rollback normal
identité opérateur/authentication inventée

21. Critères de fermeture du gate pre.001

Après ce plan, le gate conceptuel est considéré fermé lorsque la validation associée confirme :

base stable cohérente
règles relues et audits statiques propres
contrats Store API proposés et bornés
conflict lifecycle complet défini
revision/CAS et races définis
migration V004 justifiée et additive
rétention/pins/rollback définis
Transient/Terminal défini sans texte libre
Worker retry/backpressure/Blocked défini
Transport reconnect réutilise les mécanismes existants
Backfill reste indépendant et compatible
preuves techniques/live identifiées
prereleases recalibrées
hors périmètre explicite

L'implémentation commence seulement en 0.3.17-pre.002.