diff --git a/Cargo.toml b/Cargo.toml index 82ab955..f9784c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 509 +# version: 510 [workspace] resolver = "3" members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-solprices-desk", "crates/ksp-app-store-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-interface-lib", "crates/ksp-job-api", "crates/ksp-job-backfill-lib", "crates/ksp-logging-lib", "crates/ksp-offchain-transport-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-program-api", "crates/ksp-raw-transaction-lib", "crates/ksp-store-api", "crates/ksp-store-lib", "crates/ksp-store-postgres-lib", "crates/ksp-wallet-lib", "crates/ksp-worker-api", "crates/ksp-worker-raw-transaction-ingest-lib"] [workspace.package] -version = "0.3.11-pre.4" +version = "0.3.11-pre.5" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs index 974507d..3220126 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs -// version: 3 +// version: 4 #![warn(missing_docs)] #![deny(unreachable_pub)] @@ -8,9 +8,9 @@ //! Source-neutral runtime foundation for continuous KSP RAW transaction ingestion. //! //! This tranche owns the concrete Worker family identity, validated technical settings -//! and the minimal caller-runtime-owned start/stop lifecycle. Task supervision, bounded -//! admission, persistence and latest-value snapshots remain in their dedicated prereleases; -//! no live source or Transport dependency exists here. +//! and the caller-runtime-owned lifecycle with private child-task supervision. Bounded +//! admission, persistence and latest-value snapshots remain in their dedicated prereleases; no +//! live source or Transport dependency exists here. mod error; mod identity; diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs index dce5ccd..c51de2b 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs -// version: 1 +// version: 2 /// Runtime-neutral boxed future resolving after one RAW transaction ingest Worker has fully reached a terminal lifecycle state. pub type RawTransactionIngestTerminalFuture<'a> = @@ -85,6 +85,25 @@ fn current_runtime_handle() -> ksp_core_lib::Result { }; } +async fn drain_children(children: &mut tokio::task::JoinSet<()>) -> bool { + let mut clean = true; + while let std::option::Option::Some(joined) = children.join_next().await { + if joined.is_err() { + clean = false; + } + } + return clean; +} + +fn finish_faulted(lifecycle: &mut ksp_worker_api::WorkerLifecycle, sender: &tokio::sync::watch::Sender) { + if lifecycle.fault(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID).is_err() { + sender.send_replace(ksp_worker_api::WorkerState::Faulted(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID)); + return; + } + sender.send_replace(lifecycle.state()); + return; +} + fn finish_stopped(lifecycle: &mut ksp_worker_api::WorkerLifecycle, sender: &tokio::sync::watch::Sender) { if lifecycle.mark_stopping().is_err() { sender.send_replace(ksp_worker_api::WorkerState::Faulted(crate::ERROR_CODE_RAW_TRANSACTION_INGEST_RUNTIME_INVALID)); @@ -99,12 +118,15 @@ fn finish_stopped(lifecycle: &mut ksp_worker_api::WorkerLifecycle, sender: &toki return; } -async fn run_foundation( +async fn run_supervisor( mut lifecycle: ksp_worker_api::WorkerLifecycle, _store_guard: std::option::Option>, mut stop_receiver: tokio::sync::watch::Receiver, terminal_sender: tokio::sync::watch::Sender, -) { + source_spawner: Spawner, +) where + Spawner: FnOnce(&mut tokio::task::JoinSet<()>, tokio::sync::watch::Receiver) + std::marker::Send + 'static, +{ if *stop_receiver.borrow() { finish_stopped(&mut lifecycle, &terminal_sender); return; @@ -114,11 +136,12 @@ async fn run_foundation( return; } terminal_sender.send_replace(lifecycle.state()); - loop { - let changed = stop_receiver.changed().await; - if changed.is_err() || *stop_receiver.borrow() { - break; - } + let mut children = tokio::task::JoinSet::new(); + source_spawner(&mut children, stop_receiver.clone()); + let supervised_clean = supervise_until_stop(&mut stop_receiver, &mut children).await; + if !supervised_clean { + finish_faulted(&mut lifecycle, &terminal_sender); + return; } finish_stopped(&mut lifecycle, &terminal_sender); return; @@ -129,6 +152,18 @@ fn start_foundation( runtime: tokio::runtime::Handle, store_guard: std::option::Option>, ) -> ksp_core_lib::Result { + return start_foundation_with_source_spawner(settings, runtime, store_guard, |_children, _stop_receiver| {}); +} + +fn start_foundation_with_source_spawner( + settings: crate::RawTransactionIngestSettings, + runtime: tokio::runtime::Handle, + store_guard: std::option::Option>, + source_spawner: Spawner, +) -> ksp_core_lib::Result +where + Spawner: FnOnce(&mut tokio::task::JoinSet<()>, tokio::sync::watch::Receiver) + std::marker::Send + 'static, +{ let kind = match ksp_worker_api::WorkerKindCode::new(crate::RAW_TRANSACTION_INGEST_WORKER_KIND_CODE) { std::result::Result::Ok(value) => value, std::result::Result::Err(_) => return std::result::Result::Err(crate::runtime_error("start.worker_kind_invalid")), @@ -141,10 +176,41 @@ fn start_foundation( let (stop_sender, stop_receiver) = tokio::sync::watch::channel(false); let (terminal_sender, terminal_receiver) = tokio::sync::watch::channel(lifecycle.state()); let handle = crate::RawTransactionIngestHandle { stop_sender, stop_token, terminal_receiver }; - std::mem::drop(runtime.spawn(run_foundation(lifecycle, store_guard, stop_receiver, terminal_sender))); + std::mem::drop(runtime.spawn(run_supervisor(lifecycle, store_guard, stop_receiver, terminal_sender, source_spawner))); return std::result::Result::Ok(handle); } +async fn supervise_until_stop(stop_receiver: &mut tokio::sync::watch::Receiver, children: &mut tokio::task::JoinSet<()>) -> bool { + let mut clean = true; + loop { + if *stop_receiver.borrow() { + break; + } + if children.is_empty() { + let changed = stop_receiver.changed().await; + if changed.is_err() || *stop_receiver.borrow() { + break; + } + continue; + } + tokio::select! { + biased; + changed = stop_receiver.changed() => { + if changed.is_err() || *stop_receiver.borrow() { + break; + } + } + joined = children.join_next() => { + if let std::option::Option::Some(std::result::Result::Err(_)) = joined { + clean = false; + } + } + } + } + let drained_clean = drain_children(children).await; + return clean && drained_clean; +} + fn validate_store_network(settings: &crate::RawTransactionIngestSettings, store_network: &ksp_store_lib::RawNetworkId) -> ksp_core_lib::Result<()> { if settings.network() != store_network { return std::result::Result::Err(crate::runtime_error("start.store_network_mismatch")); diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs b/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs index 0b65269..39321e3 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs @@ -1,5 +1,5 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs -// version: 3 +// version: 4 //! Dependency firewall canaries for the RAW transaction ingest Worker foundation. @@ -52,22 +52,14 @@ fn pre_002_manifest_keeps_forbidden_layers_and_live_sources_out() { } #[test] -fn pre_004_source_surface_opens_lifecycle_without_supervisor_admission_or_live_sources() { +fn pre_005_source_surface_owns_private_supervisor_without_admission_persistence_or_live_sources() { let root = include_str!("../src/lib.rs"); let runtime = include_str!("../src/runtime.rs"); - for required in [ - "mod runtime;", - "pub use self::runtime::RawTransactionIngestHandle;", - "pub use self::runtime::RawTransactionIngestTerminalFuture;", - "pub use self::runtime::RawTransactionIngestWorker;", - "pub fn start(", - "pub fn request_stop(&self) -> bool", - "pub fn wait_terminal(&self)", - ] { - assert!(root.contains(required) || runtime.contains(required), "required pre.004 runtime contract missing: {required}"); + for required in ["tokio::task::JoinSet", "run_supervisor", "supervise_until_stop", "drain_children", "start_foundation_with_source_spawner"] { + assert!(runtime.contains(required), "required pre.005 private supervisor contract missing: {required}"); } for forbidden in [ - "JoinSet", + "pub use self::runtime::JoinSet", "mpsc::", "canonicalize_raw_transaction", "persist_raw_transaction", @@ -75,7 +67,7 @@ fn pre_004_source_surface_opens_lifecycle_without_supervisor_admission_or_live_s "RawTransactionIngestSnapshot", "WorkerSnapshotSource", ] { - assert!(!root.contains(forbidden) && !runtime.contains(forbidden), "pre.004 opened later runtime scope too early: {forbidden}"); + assert!(!root.contains(forbidden) && !runtime.contains(forbidden), "pre.005 opened later runtime scope too early: {forbidden}"); } return; } diff --git a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs index 803594d..ef61f89 100644 --- a/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs +++ b/crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs @@ -1,5 +1,23 @@ // file: crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs -// version: 1 +// version: 2 + +struct ActiveTaskGuard { + active: std::sync::Arc, +} + +impl ActiveTaskGuard { + fn new(active: std::sync::Arc) -> Self { + active.fetch_add(1, std::sync::atomic::Ordering::AcqRel); + return Self { active }; + } +} + +impl std::ops::Drop for ActiveTaskGuard { + fn drop(&mut self) { + self.active.fetch_sub(1, std::sync::atomic::Ordering::AcqRel); + return; + } +} fn settings(network: &str) -> std::option::Option { let network = match ksp_store_lib::RawNetworkId::new(network) { @@ -13,6 +31,16 @@ fn settings(network: &str) -> std::option::Option, expected: usize) -> bool { + for _ in 0..64 { + if active.load(std::sync::atomic::Ordering::Acquire) == expected { + return true; + } + tokio::task::yield_now().await; + } + return active.load(std::sync::atomic::Ordering::Acquire) == expected; +} + #[test] fn pre_004_current_runtime_is_required_before_spawn() { let result = super::current_runtime_handle(); @@ -125,3 +153,101 @@ async fn pre_004_dropping_last_control_handle_causes_private_runtime_exit() { } } } + +#[tokio::test(flavor = "current_thread")] +async fn pre_005_supervisor_joins_all_cooperative_source_tasks_before_terminal() { + let settings = match settings("mainnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let source_active = std::sync::Arc::clone(&active); + let handle = match super::start_foundation_with_source_spawner( + settings, + tokio::runtime::Handle::current(), + std::option::Option::None, + move |children, stop_receiver| { + for _ in 0..3 { + let active = std::sync::Arc::clone(&source_active); + let mut child_stop = stop_receiver.clone(); + let _abort_handle = children.spawn(async move { + let _guard = ActiveTaskGuard::new(active); + loop { + let changed = child_stop.changed().await; + if changed.is_err() || *child_stop.borrow() { + return; + } + } + }); + } + }, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert!(wait_for_active_count(&active, 3).await); + assert!(handle.request_stop()); + let terminal = handle.wait_terminal().await; + let terminal = match terminal { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(terminal, ksp_worker_api::WorkerState::Stopped); + assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0); + return; +} + +#[tokio::test(flavor = "current_thread")] +async fn pre_005_supervisor_reaps_completed_source_task_and_still_joins_live_child_on_stop() { + let settings = match settings("mainnet") { + std::option::Option::Some(value) => value, + std::option::Option::None => return, + }; + let active = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let completed = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)); + let source_active = std::sync::Arc::clone(&active); + let source_completed = std::sync::Arc::clone(&completed); + let handle = match super::start_foundation_with_source_spawner( + settings, + tokio::runtime::Handle::current(), + std::option::Option::None, + move |children, stop_receiver| { + let completed = std::sync::Arc::clone(&source_completed); + let _completed_abort_handle = children.spawn(async move { + completed.fetch_add(1, std::sync::atomic::Ordering::AcqRel); + return; + }); + let active = std::sync::Arc::clone(&source_active); + let mut child_stop = stop_receiver.clone(); + let _active_abort_handle = children.spawn(async move { + let _guard = ActiveTaskGuard::new(active); + loop { + let changed = child_stop.changed().await; + if changed.is_err() || *child_stop.borrow() { + return; + } + } + }); + }, + ) { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert!(wait_for_active_count(&active, 1).await); + for _ in 0..64 { + if completed.load(std::sync::atomic::Ordering::Acquire) == 1 { + break; + } + tokio::task::yield_now().await; + } + assert_eq!(completed.load(std::sync::atomic::Ordering::Acquire), 1); + assert!(handle.request_stop()); + let terminal = handle.wait_terminal().await; + let terminal = match terminal { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + assert_eq!(terminal, ksp_worker_api::WorkerState::Stopped); + assert_eq!(active.load(std::sync::atomic::Ordering::Acquire), 0); + return; +} diff --git a/deltas/0.3.11/pre.005.md b/deltas/0.3.11/pre.005.md new file mode 100644 index 0000000..f2c14da --- /dev/null +++ b/deltas/0.3.11/pre.005.md @@ -0,0 +1,125 @@ + + + +# Delta `0.3.11-pre.005` — supervisor privé et ownership des tâches + +## Base requise + +```text +0.3.11-pre.004 +workspace.package.version = 0.3.11-pre.4 +``` + +Le gate opérateur du 8 septembre 2026 est vert sur `fmt`, audits Rust/Markdown, `cargo check --workspace`, Clippy strict, les 16 tests de la crate Worker, doc-tests et les arbres Cargo normal/features. + +## Objectif + +Matérialiser uniquement la responsabilité `pre.005` du plan `032` : supervisor privé, `JoinSet` privé, ownership et join des tâches enfants, intégration du stop au seam source déterministe et preuve qu'aucune tâche enfant coopérative ne survit au terminal. Aucun admission `mpsc`, aucune canonicalisation RAW et aucune persistence Store réelle. + +## Version + +```text +identifiant de livraison : 0.3.11-pre.005 +workspace.package.version : 0.3.11-pre.5 +``` + +## Runtime privé ajouté + +Le task racine devient le supervisor du run. Il possède un `tokio::task::JoinSet<()>` privé et un seam privé de spawn de tâches source. + +La production appelle ce seam sans source. Les tests injectent des tâches déterministes qui reçoivent un clone du `watch` de stop. + +Le supervisor : + +```text +reap des enfants terminés via join_next() +stop prioritaire via select! biased +join de tous les enfants restants après stop/fermeture du contrôle +Stopped publié uniquement après join complet +JoinError -> supervision non clean ; Faulted à la fermeture de supervision +``` + +Aucun `JoinHandle` ou `JoinSet` n'est ajouté à la surface publique. + +## Arrêt coopératif + +Le timeout de drain configuré existe déjà dans les settings mais n'est pas encore appliqué ici. `pre.005` attend coopérativement les enfants ; le timeout, `abort_all` et les races stop/fault sont réservés au hardening `pre.009`. + +## Hors scope conservé + +```text +mpsc / admission +PrivateRawTransactionIngress +canonicalisation common RAW +observation key +persistence Store +snapshots concrets / WorkerSnapshotSource +source live / Transport +Config +abort/timeout de drain +``` + +## Tests ajoutés ou étendus + +```text +supervisor joint trois tâches source coopératives avant Stopped +compteur d'enfants actifs == 0 au terminal +reap d'une source terminée pendant Running +join d'une source encore active lors du stop +canary statique : JoinSet présent mais privé +canary statique : aucun scope pre.006+ ouvert +``` + +## Fichiers ajoutés + +```text +deltas/0.3.11/pre.005.md +``` + +## Fichiers modifiés + +```text +Cargo.toml +crates/ksp-worker-raw-transaction-ingest-lib/src/lib.rs +crates/ksp-worker-raw-transaction-ingest-lib/src/runtime.rs +crates/ksp-worker-raw-transaction-ingest-lib/tests/dependency_boundary.rs +crates/ksp-worker-raw-transaction-ingest-lib/unit_tests/runtime.rs +docs/plans/032-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION_PLAN.md +docs/validation/028-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION.md +``` + +## Fichiers supprimés + +Aucun. + +## Validations exécutées dans l'environnement d'assemblage + +```text +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +scan statique des interdits/scope runtime +Markdown table audit: clean (340 table(s), 780 file(s)) +``` + +L'environnement d'assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Aucun gate Cargo de `pre.005` n'est déclaré PASS localement. + +## Gate opérateur demandé + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-worker-raw-transaction-ingest-lib +cargo tree -p ksp-worker-raw-transaction-ingest-lib --edges normal +cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features +``` + +## Décision + +`pre.006` reste bloquée jusqu'à validation opérateur verte de cette tranche. + +## Questions ouvertes + +Aucune nouvelle question architecturale. L'admission bornée et la canonicalisation common RAW restent dans `pre.006`, la persistence dans `pre.007`, les snapshots dans `pre.008` et le drain forcé/races dans `pre.009`. diff --git a/docs/plans/032-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION_PLAN.md b/docs/plans/032-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION_PLAN.md index b4ab63d..02a7434 100644 --- a/docs/plans/032-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION_PLAN.md +++ b/docs/plans/032-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION_PLAN.md @@ -1,5 +1,5 @@ - + # Plan v0.3.11 — fondation runtime du Worker RawTransaction ingest @@ -711,7 +711,7 @@ Budget cible : **10–15 min**. Kind code, settings, defaults/bounds, validation Budget cible : **15–20 min**. Start sur runtime caller-owned, handle public, stop idempotent, lifecycle générique, terminal future sans JoinHandle public. Harness runtime minimal. -État après matérialisation : **implémenté, gate opérateur requis**. `RawTransactionIngestWorker::start(settings, Arc)` vérifie le runtime Tokio courant puis le réseau Store avant spawn. Le handle clonable expose uniquement `request_stop()` et `wait_terminal()` ; un `watch` privé sert au wake-up de stop et un `watch` privé au terminal de fondation. Le task racine ne possède encore aucun task enfant, aucune admission/persistence et aucun snapshot concret ; la disparition de tous les handles ferme le sender de stop et fait terminer proprement ce task. `wait_terminal()` attend le terminal puis la fermeture du sender d'état, sans exposer de `JoinHandle`. +État après matérialisation : **implémenté et gate opérateur validé**. `RawTransactionIngestWorker::start(settings, Arc)` vérifie le runtime Tokio courant puis le réseau Store avant spawn. Le handle clonable expose uniquement `request_stop()` et `wait_terminal()` ; un `watch` privé sert au wake-up de stop et un `watch` privé au terminal de fondation. Le task racine ne possédait encore aucun task enfant, aucune admission/persistence et aucun snapshot concret ; la disparition de tous les handles ferme le sender de stop et fait terminer proprement ce task. `wait_terminal()` attend le terminal puis la fermeture du sender d'état, sans exposer de `JoinHandle`. Le gate communiqué le 8 septembre 2026 est vert sur `fmt`, audits, `check`, Clippy strict, 16 tests de crate, doc-tests et les deux arbres Cargo. Les méthodes `snapshot_source()` et `worker_snapshot_source()` prévues par la surface finale restent volontairement différées à `pre.008`, où leur contrat latest-value concret sera effectivement prouvé. @@ -719,6 +719,8 @@ Les méthodes `snapshot_source()` et `worker_snapshot_source()` prévues par la Budget cible : **15–20 min**. Introduire le supervisor, les JoinSet/joins privés, intégrer le wake-up de stop déjà matérialisé à l'ownership des tâches enfants, ajouter le test source seam et prouver qu'aucune tâche enfant ne survit au terminal. Pas encore de persistence réelle. +État après matérialisation : **implémenté, gate opérateur requis**. Le task racine est désormais le supervisor privé et possède un `tokio::task::JoinSet<()>`. Un seam privé de spawn de sources reçoit le `JoinSet` et un clone du `watch` de stop ; la production utilise un seam vide tandis que les unit tests injectent des tâches coopératives déterministes. Le supervisor récole les enfants terminés pendant l'exécution puis, au stop ou à la fermeture du channel de contrôle, attend la fin de tous les enfants avant de publier `Stopped`. Un `JoinError` observé marque la supervision comme non clean ; si la fermeture de supervision est ensuite engagée, le terminal est projeté vers `Faulted(worker_raw_transaction_ingest.runtime_invalid)`. Le traitement immédiat/prioritaire des faults reste différé à `pre.009`. Aucun timeout/abort forcé n'est encore introduit : ce hardening reste réservé à `pre.009`. + ### `pre.006` — admission bornée + canonicalisation common Budget cible : **15–20 min**. `mpsc` borné, backpressure, ingress privé, common `RawTransactionMaterial -> RawTransaction`, observation-key domain/golden et assembly. Pas de source réseau. diff --git a/docs/validation/028-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION.md b/docs/validation/028-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION.md index 992dd9a..ca95f8c 100644 --- a/docs/validation/028-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION.md +++ b/docs/validation/028-V0_3_11_RAW_TRANSACTION_INGEST_WORKER_FOUNDATION.md @@ -1,5 +1,5 @@ - + # Validation v0.3.11 — fondation runtime du Worker RawTransaction ingest @@ -740,3 +740,115 @@ cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features Critère de passage : le start caller-owned, le contrôle de réseau, les transitions generic Worker API, le stop idempotent et le terminal boxed sont verts ; aucun `JoinHandle` public, supervisor enfant, admission, Store write, snapshot concret ou source live n'apparaît. +## 16. Fermeture opérateur `pre.004` et matérialisation `pre.005` + +### 16.1 Fermeture opérateur de `pre.004` + +Le journal opérateur communiqué le 8 septembre 2026 ferme `0.3.11-pre.004` (`workspace.package.version = 0.3.11-pre.4`) : + +```text +cargo fmt --all : terminé sans erreur +python3 scripts/audit_rust_workspace_rules.py : clean, export completeness 0 +python3 scripts/audit_markdown_tables.py ... : clean (340 tables, 779 files) +cargo check --workspace : terminé sans erreur +cargo clippy --workspace --all-targets --all-features -- -D warnings : terminé sans erreur +cargo test -p ksp-worker-raw-transaction-ingest-lib : 9 unit + 3 dependency-boundary + 4 public API PASS, 0 échec +doc-tests : PASS +cargo tree normal : conforme au firewall attendu +cargo tree features : Tokio limité à macros/rt/sync/time côté Worker et Store sans backend Worker direct +``` + +`pre.005` peut donc être ouverte. + +### 16.2 Scope matérialisé dans `pre.005` + +Le task racine de fondation devient explicitement le supervisor privé. Il possède : + +```text +tokio::task::JoinSet<()> +watch de stop déjà introduit en pre.004 +seam privé de spawn de tâches source +Store Arc guard déjà introduit en pre.004 +``` + +Le seam de production est vide : aucune source live n'existe dans `0.3.11`. Les tests unitaires utilisent le même seam privé pour injecter des tâches coopératives déterministes qui observent le `watch` de stop. + +### 16.3 Ownership et ordre terminal + +Le supervisor : + +```text +1. publie Running ; +2. installe les tâches enfants du seam privé ; +3. récole les enfants terminés via JoinSet::join_next() ; +4. observe le stop en priorité via tokio::select! biased ; +5. après stop/fermeture du channel, attend tous les enfants restants ; +6. publie seulement ensuite Stopping puis Stopped. +``` + +Aucun `JoinHandle`/`JoinSet` n'est public. Le terminal `Stopped` ne peut donc pas précéder la fin des tâches enfants supervisées dans le harness coopératif. + +Un `JoinError` d'une tâche enfant marque la supervision comme non clean ; lors d'une fermeture de supervision, le terminal est alors classé avec le code stable `worker_raw_transaction_ingest.runtime_invalid`. Le traitement immédiat/prioritaire des faults, des races stop/fault, du timeout de drain et de l'`abort_all` reste volontairement dans `pre.009`. + +### 16.4 Frontière volontaire de `pre.005` + +La tranche n'introduit encore aucun : + +```text +mpsc / admission +PrivateRawTransactionIngress +RawTransactionMaterial -> RawTransaction +observation key +appel Store de persistence +concurrency de persistence +snapshot concret +source live +Transport +Config +timeout/abort forcé de drain +``` + +`pre.006` reste propriétaire de l'admission bornée et de la canonicalisation common RAW. `pre.007` reste propriétaire de la persistence réelle. + +### 16.5 Preuves unitaires ajoutées + +Les tests du seam privé prouvent : + +```text +trois tâches source coopératives sont toutes actives avant stop +le terminal Stopped n'est obtenu qu'après retour à zéro du compteur de tâches actives +une tâche source déjà terminée est récollectée sans empêcher le supervisor de rester vivant +une autre tâche encore active est jointe lors du stop +``` + +### 16.6 Preuves locales d'assemblage `pre.005` + +Exécuté dans l'environnement d'assemblage : + +```text +python3 scripts/audit_rust_workspace_rules.py +General Rust rule audit: clean +Rust export completeness audit: 0 candidate(s) +KSP workspace Rust rule audit: clean + +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +Markdown table audit: clean (340 table(s), 780 file(s)) +``` + +L'environnement d'assemblage ne fournit ni `cargo`, ni `rustc`, ni `rustfmt`. Aucun gate Cargo `pre.005` n'est déclaré PASS localement. + +### 16.7 Gate opérateur demandé pour `pre.005` + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +python3 scripts/audit_markdown_tables.py README.md RULES.md ROADMAP.md CHANGELOG.md docs prompts crates deltas +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-worker-raw-transaction-ingest-lib +cargo tree -p ksp-worker-raw-transaction-ingest-lib --edges normal +cargo tree -p ksp-worker-raw-transaction-ingest-lib -e features +``` + +Critère de passage : le supervisor/JoinSet privé, le seam source déterministe et le join complet avant terminal sont verts ; aucun `mpsc`, common RAW assembly, Store write, snapshot concret, source live ou Transport n'apparaît. +