From b1b0e916164229a4da0fa3e5d63539cc24bb23bb Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Fri, 18 Sep 2026 23:33:08 +0200 Subject: [PATCH] 0.3.15-pre.015-fix.001 --- Cargo.toml | 4 +- crates/ksp-onchain-transport-lib/README.md | 4 +- .../src/grpc_stream.rs | 10 +- .../tests/release_completeness.rs | 24 ++++- .../unit_tests/grpc_stream.rs | 22 ++++- deltas/0.3.15/pre.015-fix.001.md | 99 +++++++++++++++++++ ...032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md | 22 ++++- 7 files changed, 173 insertions(+), 12 deletions(-) create mode 100644 deltas/0.3.15/pre.015-fix.001.md diff --git a/Cargo.toml b/Cargo.toml index a1552e9..1f7b021 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 632 +# version: 633 [workspace] resolver = "3" members = ["crates/ksp-app-backfill-desk", "crates/ksp-app-config-desk", "crates/ksp-app-raw-transaction-ingest-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.15-pre.15" +version = "0.3.15-pre.15.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/README.md b/crates/ksp-onchain-transport-lib/README.md index 71a6035..f9ff89a 100644 --- a/crates/ksp-onchain-transport-lib/README.md +++ b/crates/ksp-onchain-transport-lib/README.md @@ -1,5 +1,5 @@ - + # `ksp-onchain-transport-lib` @@ -239,7 +239,7 @@ YellowstoneGrpcSubscribeSnapshot Les sept unary standards retenus sont `SubscribeReplayInfo`, `Ping`, `GetLatestBlockhash`, `GetBlockHeight`, `GetSlot`, `IsBlockhashValid` et `GetVersion`. `Subscribe` couvre accounts, slots, transactions, transaction status, blocks, block metadata et entries, avec `commitment`, `accounts_data_slice`, `ping` et `from_slot`. `SubscribeDeshred` reste hors scope de la foundation standard. -Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. La queue d'updates Yellowstone reste bornée mais sa saturation locale n'est plus un fault : l'acteur attend asynchronement qu'une place soit rendue et cesse donc de poller le stream Tonic, ce qui propage la backpressure via gRPC/HTTP/2 sans drop silencieux ni queue non bornée. Le shutdown préempte cette attente. La queue de mutations de requête reste au contraire synchrone/fail-fast : un caller qui la sature reçoit toujours `grpc_backpressure_overflow`. +Le stream bidirectionnel est borné : request/update queues, tailles inbound/outbound, half-close, close timeout et reconnect budget sont explicites. La queue d'updates Yellowstone reste bornée mais sa saturation locale n'est plus un fault : l'acteur attend asynchronement qu'une place soit rendue et cesse donc de poller le stream Tonic, ce qui propage la backpressure via gRPC/HTTP/2 sans drop silencieux ni queue non bornée. Le shutdown préempte cette attente. La queue de mutations de requête reste au contraire synchrone/fail-fast : un caller qui la sature reçoit toujours `grpc_backpressure_overflow`. Pendant un shutdown local explicite, un `Status` gRPC reçu après le half-close client est traité comme une terminaison `Closed` de cette fermeture déjà engagée ; le même `Status` reçu pendant une session active conserve son comportement normal de reconnect/fault et n'est pas masqué. Après reconnect, KSP rejoue la dernière requête complète acceptée et avance prudemment `from_slot` selon le dernier slot observé et `SubscribeReplayInfo.first_available`. Les compteurs de gap et duplicate sont de l’observabilité ; ils ne constituent jamais une garantie exactly-once ou lossless. diff --git a/crates/ksp-onchain-transport-lib/src/grpc_stream.rs b/crates/ksp-onchain-transport-lib/src/grpc_stream.rs index 96374e3..9da99fa 100644 --- a/crates/ksp-onchain-transport-lib/src/grpc_stream.rs +++ b/crates/ksp-onchain-transport-lib/src/grpc_stream.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/grpc_stream.rs -// version: 6 +// version: 7 use tonic_prost::prost::Message; // rust-rules: trait-import @@ -1012,16 +1012,16 @@ async fn finish_client_half_close( }, std::result::Result::Ok(std::result::Result::Err(status)) => { let code = status.code().to_string(); - ksp_logging_lib::warn!( + ksp_logging_lib::debug!( target: crate::TRACING_TARGET, endpoint_name, provider = provider.as_str(), cluster = cluster.as_str(), grpc_code = code.as_str(), - "Yellowstone subscribe endpoint returned a status during graceful shutdown" + "Yellowstone subscribe endpoint returned a status after local graceful shutdown request; treating session as closed" ); - snapshot.state = crate::YellowstoneGrpcSubscribeState::Failed; - snapshot.terminal_error_code = std::option::Option::Some(crate::ERROR_CODE_GRPC_STATUS); + snapshot.state = crate::YellowstoneGrpcSubscribeState::Closed; + snapshot.terminal_error_code = std::option::Option::None; snapshot_tx.send_replace(*snapshot); return; }, diff --git a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs index aba0d65..626240c 100644 --- a/crates/ksp-onchain-transport-lib/tests/release_completeness.rs +++ b/crates/ksp-onchain-transport-lib/tests/release_completeness.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs -// version: 48 +// version: 49 //! Release-level completeness canaries for staged HTTP and WebSocket Transport coverage. @@ -1434,3 +1434,25 @@ enum ReconnectOutcome", assert!(!stream_source.contains("unbounded_channel")); assert!(stream_tests.contains("yellowstone_slow_receiver_applies_bounded_backpressure_without_terminal_overflow")); } + +#[test] +fn release_v0_3_15_pre_015_fix_001_local_close_status_is_closed_without_weakening_active_status_failure() { + let stream_source = include_str!("../src/grpc_stream.rs"); + let stream_tests = include_str!("../unit_tests/grpc_stream.rs"); + let close_start = stream_source.find("async fn finish_client_half_close(").expect("graceful half-close helper must remain present"); + let close_tail = &stream_source[close_start..]; + let close_end = close_tail.find("\nfn stream_status_error(").expect("graceful half-close helper boundary must remain present"); + let close_helper = &close_tail[..close_end]; + let actor_start = stream_source.find("async fn run_subscribe_actor(").expect("Subscribe actor must remain present"); + let actor_tail = &stream_source[actor_start..]; + let actor_end = actor_tail.find("\nenum ReconnectOutcome").expect("Subscribe actor boundary must remain present"); + let actor = &actor_tail[..actor_end]; + assert!(close_helper.contains("returned a status after local graceful shutdown request; treating session as closed")); + assert!(close_helper.contains("snapshot.state = crate::YellowstoneGrpcSubscribeState::Closed")); + assert!(close_helper.contains("snapshot.terminal_error_code = std::option::Option::None")); + assert!(!close_helper.contains("snapshot.terminal_error_code = std::option::Option::Some(crate::ERROR_CODE_GRPC_STATUS)")); + assert!(actor.contains("stream_status_error(\"Subscribe\", status")); + assert!(actor.contains("fail_actor(crate::ERROR_CODE_GRPC_STATUS")); + assert!(stream_tests.contains("yellowstone_explicit_close_accepts_remote_status_after_local_half_close")); + assert!(stream_tests.contains("yellowstone_stream_remote_status_is_safe_and_terminal")); +} diff --git a/crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs b/crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs index bcccd1e..85ac17d 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs @@ -1,10 +1,11 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs -// version: 7 +// version: 8 #[derive(Clone, Copy)] enum FixtureMode { RoundTrip, ClientHalfClose, + ClientHalfCloseStatus, HostileClose, Flood, RemoteStatus, @@ -86,6 +87,13 @@ impl yellowstone_grpc_proto::geyser::geyser_server::Geyser for FixtureGeyser { half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst); } }, + FixtureMode::ClientHalfCloseStatus => { + let half_close = inbound.message().await; + if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) { + half_close_seen.store(true, std::sync::atomic::Ordering::SeqCst); + } + let _ = outbound_tx.send(std::result::Result::Err(tonic::Status::unknown("GRPC-CLOSE-STATUS-SECRET-CANARY"))).await; + }, FixtureMode::HostileClose => { let half_close = inbound.message().await; if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) { @@ -465,6 +473,18 @@ async fn yellowstone_explicit_close_half_closes_request_stream_before_deadline() server.stop().await; } +#[tokio::test(flavor = "current_thread")] +async fn yellowstone_explicit_close_accepts_remote_status_after_local_half_close() { + let server = FixtureServer::start(FixtureMode::ClientHalfCloseStatus).await; + let defaults = crate::YellowstoneGrpcSessionSettings::default(); + let settings = fixture_settings(server.endpoint_url.as_str(), 8, 8, defaults.max_inbound_message_size_bytes(), defaults.max_outbound_message_size_bytes()); + let channel = crate::YellowstoneGrpcChannel::connect(&settings).await.expect("fixture channel must connect"); + let session = channel.open_standard_subscribe(initial_request()).await.expect("fixture Subscribe stream must open"); + session.close().await.expect("remote status after local half-close must remain a successful graceful close"); + assert!(server.half_close_seen.load(std::sync::atomic::Ordering::SeqCst)); + server.stop().await; +} + #[tokio::test(flavor = "current_thread")] async fn yellowstone_hostile_server_shutdown_is_bounded_by_close_timeout() { let server = FixtureServer::start(FixtureMode::HostileClose).await; diff --git a/deltas/0.3.15/pre.015-fix.001.md b/deltas/0.3.15/pre.015-fix.001.md new file mode 100644 index 0000000..edc2fa2 --- /dev/null +++ b/deltas/0.3.15/pre.015-fix.001.md @@ -0,0 +1,99 @@ + + + +# Delta v0.3.15-pre.015-fix.001 — Stop Yellowstone post-half-close + +## Base + +```text +ksp-general-0.3.15-pre.015.zip +SHA-256: 3ab4b976ae612286b577aa003c10e6b9627be040a0a0fc51b2c9e45d6616893e +``` + +Le gate `pre.015` confirme la correction de la saturation locale : les audits, `cargo check`, Clippy, les suites ciblées et le workspace passent, le canari Transport de backpressure soutenue passe, et la trace live fournie ne contient plus de `grpc_backpressure_overflow` Yellowstone pendant le run. Le défaut restant appartient au même contrat `pre.015`, car le gate exige explicitement `Stop ciblé -> Stopped`. + +## Défaut observé + +```text +20:57:33.659 Stop Yellowstone demandé +20:57:33.778 PublicNode renvoie grpc Status Unknown pendant graceful shutdown +20:57:33.780 Worker classe la source source_failed / grpc_status +20:57:34.918 route terminale Faulted / Unhealthy +``` + +La fonction `finish_client_half_close()` est exécutée uniquement après déclenchement du shutdown local. Elle classait pourtant tout `Status` reçu après le half-close comme une nouvelle faute distante, ce qui remplaçait à tort la terminaison coopérative déjà engagée. + +## Correction + +Pendant `finish_client_half_close()` : + +```text +Status reçu après shutdown local explicite + -> log safe du code gRPC + -> state = Closed + -> terminal_error_code = None + -> session.close() = Ok(()) +``` + +La branche active reste inchangée : un `Status` reçu par `run_subscribe_actor()` avant shutdown continue d'entrer dans la politique de reconnect bornée et reste terminal si cette politique ne peut pas récupérer la session. + +Le timeout de fermeture hostile reste également inchangé et borné par `close_timeout`. + +## Canaris + +```text +yellowstone_explicit_close_accepts_remote_status_after_local_half_close +yellowstone_stream_remote_status_is_safe_and_terminal +yellowstone_hostile_server_shutdown_is_bounded_by_close_timeout +release_v0_3_15_pre_015_fix_001_local_close_status_is_closed_without_weakening_active_status_failure +``` + +## Frontières + +Aucun changement de Worker, Store, Config, Desk, schema, RAW, reconnect budget, capacités de queue ou API publique. Aucun status reçu en fonctionnement normal n'est reclassé en succès. + +## Version + +```text +workspace.package.version : 0.3.15-pre.15.fix.1 +root Cargo header counter : 633 +``` + +## Validation attendue + +```text +cargo fmt --all +cargo fmt --all -- --check +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/0.3.15 +cargo check --workspace +cargo clippy --workspace --all-targets --all-features -- -D warnings +cargo test -p ksp-onchain-transport-lib --all-targets --all-features +cargo test -p ksp-worker-raw-transaction-ingest-lib --all-targets --all-features +cargo test -p ksp-app-raw-transaction-ingest-desk --all-targets --all-features +cargo test --workspace --all-targets --all-features +(cd crates/ksp-app-raw-transaction-ingest-desk && cargo tauri dev) +``` + +Live : Yellowstone Mainnet doit rester sain pendant le run puis terminer `Stopped` après Stop ciblé, y compris si le provider renvoie `Unknown` après le half-close. HTTP Block Polling doit conserver son comportement `Stopped/Healthy` indépendant. + +## Inventaire exact du delta + +Ajout : + +```text +deltas/0.3.15/pre.015-fix.001.md +``` + +Modifications : + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/README.md +crates/ksp-onchain-transport-lib/src/grpc_stream.rs +crates/ksp-onchain-transport-lib/tests/release_completeness.rs +crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs +docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md +``` + +Suppressions : aucune. diff --git a/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md b/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md index de97f71..79fd556 100644 --- a/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md +++ b/docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md @@ -1,5 +1,5 @@ - + # Validation v0.3.15 — Raw Transaction Ingest Desk @@ -1052,3 +1052,23 @@ cargo test --workspace --all-targets --all-features Gate live Mainnet : Yellowstone seul pendant au moins 10 minutes, puis Yellowstone + HTTP Block Polling pendant au moins 10 minutes. Dans les deux cas, aucune occurrence de `Yellowstone subscribe update queue overflowed` ou `grpc_backpressure_overflow` n'est admise ; la route Yellowstone doit rester active/saine jusqu'au Stop ciblé, puis terminer `Stopped`. Une interruption réseau distante réellement observée doit rester classée par les codes Transport/reconnect existants et ne doit pas être confondue avec ce gate de saturation locale. +### `pre.015-fix.001` — Stop Yellowstone : `Status` post-half-close non terminal + +Le gate opérateur de `pre.015` est propre sur les audits, `cargo check --workspace`, Clippy strict, Transport (`395/395` unitaires dont le canari de backpressure soutenue), Worker (`161/161` unitaires), Raw Transaction Ingest Desk et le workspace. Le live Mainnet ne reproduit plus `Yellowstone subscribe update queue overflowed` ni `grpc_backpressure_overflow` dans la trace fournie. + +Le défaut restant apparaît uniquement au Stop ciblé Yellowstone. L'opérateur demande le Stop à `20:57:33.659`; après le half-close local, PublicNode renvoie environ `119 ms` plus tard un `Status` gRPC `Unknown`. `finish_client_half_close()` classait encore ce status comme `Failed/grpc_status`, puis `session.close()` remontait cette faute au Worker, qui publiait `source_failed` et terminait `Faulted/Unhealthy`. HTTP Block Polling, arrêté ensuite, termine normalement `Stopped/Healthy`. + +Le fix corrige uniquement la sémantique de fermeture déjà engagée : `finish_client_half_close()` n'est appelé qu'après un shutdown local explicite. Un `Status` reçu dans cette phase publie désormais `Closed` avec `terminal_error_code=None`. Il ne peut donc plus remplacer le Stop coopératif par une faute distante tardive. Un timeout de fermeture reste borné et continue de produire le code `timeout`; aucune attente infinie n'est introduite. + +Cette règle ne s'applique pas aux `Status` reçus pendant une session active. La branche normale de `run_subscribe_actor()` conserve le reconnect borné existant et, lorsque la politique ne permet pas de reprise, le code `grpc_status` reste terminal. Le fix ne masque donc aucune panne distante observée avant le Stop. + +Canaris : + +```text +yellowstone_explicit_close_accepts_remote_status_after_local_half_close +yellowstone_stream_remote_status_is_safe_and_terminal +release_v0_3_15_pre_015_fix_001_local_close_status_is_closed_without_weakening_active_status_failure +``` + +Gate live attendu : Yellowstone Mainnet doit rester `Running/Healthy` pendant le run, puis un Stop ciblé doit terminer `Stopped` même si PublicNode répond `Unknown` après le half-close. Une occurrence de `grpc_status` avant la demande de Stop reste un échec réel et ne doit pas être reclassée. +