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 ;
unsafereste interdit ;unwrap,expect,panicet retours implicites restent interdits selon les lints KSP ;- aucun
pub modn'est ajouté ; - tout item partagé
pub/pub(crate)est réexporté au crate root et consommé viacrate::Itemintra-crate ; - les tests unitaires restent sous
unit_tests/, les tests d'intégration soustests/; - 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.001est 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.001utilise la version Cargo0.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
QuarantinedConflictselon 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.