0.3.15-pre.015-fix.001
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 632
|
# version: 633
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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"]
|
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]
|
[workspace.package]
|
||||||
version = "0.3.15-pre.15"
|
version = "0.3.15-pre.15.fix.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project"
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
<!-- file: crates/ksp-onchain-transport-lib/README.md -->
|
||||||
<!-- version: 25 -->
|
<!-- version: 26 -->
|
||||||
|
|
||||||
# `ksp-onchain-transport-lib`
|
# `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.
|
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.
|
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
|
// file: crates/ksp-onchain-transport-lib/src/grpc_stream.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
use tonic_prost::prost::Message; // rust-rules: trait-import
|
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)) => {
|
std::result::Result::Ok(std::result::Result::Err(status)) => {
|
||||||
let code = status.code().to_string();
|
let code = status.code().to_string();
|
||||||
ksp_logging_lib::warn!(
|
ksp_logging_lib::debug!(
|
||||||
target: crate::TRACING_TARGET,
|
target: crate::TRACING_TARGET,
|
||||||
endpoint_name,
|
endpoint_name,
|
||||||
provider = provider.as_str(),
|
provider = provider.as_str(),
|
||||||
cluster = cluster.as_str(),
|
cluster = cluster.as_str(),
|
||||||
grpc_code = code.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.state = crate::YellowstoneGrpcSubscribeState::Closed;
|
||||||
snapshot.terminal_error_code = std::option::Option::Some(crate::ERROR_CODE_GRPC_STATUS);
|
snapshot.terminal_error_code = std::option::Option::None;
|
||||||
snapshot_tx.send_replace(*snapshot);
|
snapshot_tx.send_replace(*snapshot);
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/tests/release_completeness.rs
|
// 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.
|
//! 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_source.contains("unbounded_channel"));
|
||||||
assert!(stream_tests.contains("yellowstone_slow_receiver_applies_bounded_backpressure_without_terminal_overflow"));
|
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
|
// file: crates/ksp-onchain-transport-lib/unit_tests/grpc_stream.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
enum FixtureMode {
|
enum FixtureMode {
|
||||||
RoundTrip,
|
RoundTrip,
|
||||||
ClientHalfClose,
|
ClientHalfClose,
|
||||||
|
ClientHalfCloseStatus,
|
||||||
HostileClose,
|
HostileClose,
|
||||||
Flood,
|
Flood,
|
||||||
RemoteStatus,
|
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);
|
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 => {
|
FixtureMode::HostileClose => {
|
||||||
let half_close = inbound.message().await;
|
let half_close = inbound.message().await;
|
||||||
if matches!(half_close, std::result::Result::Ok(std::option::Option::None)) {
|
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;
|
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")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn yellowstone_hostile_server_shutdown_is_bounded_by_close_timeout() {
|
async fn yellowstone_hostile_server_shutdown_is_bounded_by_close_timeout() {
|
||||||
let server = FixtureServer::start(FixtureMode::HostileClose).await;
|
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 -->
|
<!-- file: docs/validation/032-V0_3_15_RAW_TRANSACTION_INGEST_DESK.md -->
|
||||||
<!-- version: 32 -->
|
<!-- version: 33 -->
|
||||||
|
|
||||||
# Validation v0.3.15 — Raw Transaction Ingest Desk
|
# 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.
|
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