0.3.15-pre.015-fix.001

This commit is contained in:
2026-09-18 23:33:08 +02:00
parent 582a286baa
commit b1b0e91616
7 changed files with 173 additions and 12 deletions

View File

@@ -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"

View File

@@ -1,5 +1,5 @@
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
<!-- version: 25 -->
<!-- version: 26 -->
# `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 lobservabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.

View File

@@ -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;
},

View File

@@ -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"));
}

View File

@@ -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;

View File

@@ -0,0 +1,99 @@
<!-- file: deltas/0.3.15/pre.015-fix.001.md -->
<!-- version: 1 -->
# 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.

View File

@@ -1,5 +1,5 @@
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
<!-- version: 32 -->
<!-- version: 33 -->
# 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.