0.3.15-pre.015-fix.001
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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 l’observabilité ; ils ne constituent jamais une garantie exactly-once ou lossless.
|
||||
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
99
deltas/0.3.15/pre.015-fix.001.md
Normal file
99
deltas/0.3.15/pre.015-fix.001.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user