diff --git a/Cargo.toml b/Cargo.toml index 78d642a..188e510 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 201 +# version: 202 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-app-wallet-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib", "crates/ksp-onchain-transport-lib", "crates/ksp-wallet-lib"] [workspace.package] -version = "0.2.7-pre.7" +version = "0.2.7-pre.7.fix.1" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" diff --git a/crates/ksp-onchain-transport-lib/src/ws_session.rs b/crates/ksp-onchain-transport-lib/src/ws_session.rs index b780894..76e7c10 100644 --- a/crates/ksp-onchain-transport-lib/src/ws_session.rs +++ b/crates/ksp-onchain-transport-lib/src/ws_session.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/src/ws_session.rs -// version: 6 +// version: 7 use futures_util::SinkExt; // rust-rules: trait-import use futures_util::StreamExt; // rust-rules: trait-import @@ -280,7 +280,7 @@ enum WsActorIoOutcome { } enum WsReconnectOutcome { - Connected { websocket: WsPhysicalStream }, + Connected { websocket: std::boxed::Box }, ShutdownRequested { deadline: tokio::time::Instant }, HandlesDropped, Exhausted, @@ -293,7 +293,7 @@ enum WsReconnectControlOutcome { } enum WsConnectAttemptOutcome { - Connected { websocket: WsPhysicalStream, handshake_status: u16 }, + Connected { websocket: std::boxed::Box, handshake_status: u16 }, Retry, ShutdownRequested { deadline: tokio::time::Instant }, HandlesDropped, @@ -481,7 +481,7 @@ async fn run_ws_session_actor( ) .await; match recovery { - WsReconnectOutcome::Connected { websocket: replacement } => websocket = replacement, + WsReconnectOutcome::Connected { websocket: replacement } => websocket = *replacement, WsReconnectOutcome::ShutdownRequested { deadline } => { finish_disconnected_shutdown( id, @@ -522,7 +522,7 @@ async fn run_ws_session_actor( ) .await; match recovery { - WsReconnectOutcome::Connected { websocket: replacement } => websocket = replacement, + WsReconnectOutcome::Connected { websocket: replacement } => websocket = *replacement, WsReconnectOutcome::ShutdownRequested { deadline } => { finish_disconnected_shutdown( id, @@ -610,7 +610,7 @@ async fn recover_websocket_session( handshake_status, "replacement physical WebSocket connection established" ); - websocket + *websocket }, WsConnectAttemptOutcome::Retry => { attempt = attempt.saturating_add(1); @@ -645,7 +645,7 @@ async fn recover_websocket_session( continuity_gap_count = *continuity_gap_count, "physical WebSocket reconnect completed and retry budget reset" ); - return WsReconnectOutcome::Connected { websocket }; + return WsReconnectOutcome::Connected { websocket: std::boxed::Box::new(websocket) }; }, WsActorIoOutcome::ShutdownRequested { deadline } => return WsReconnectOutcome::ShutdownRequested { deadline }, WsActorIoOutcome::RemoteClosed => { @@ -872,7 +872,7 @@ async fn connect_replacement_websocket( result = &mut connect => { return match result { std::result::Result::Ok((websocket, response)) => WsConnectAttemptOutcome::Connected { - websocket, + websocket: std::boxed::Box::new(websocket), handshake_status: response.status().as_u16(), }, std::result::Result::Err(_) => { diff --git a/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs b/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs index 30b9651..59ce935 100644 --- a/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs +++ b/crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs @@ -1,5 +1,5 @@ // file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs -// version: 5 +// version: 6 use futures_util::SinkExt; // rust-rules: trait-import use futures_util::StreamExt; // rust-rules: trait-import @@ -280,17 +280,22 @@ async fn websocket_oversized_outbound_request_is_rejected_before_socket_write() } #[tokio::test(flavor = "current_thread")] -async fn websocket_oversized_inbound_frame_fails_before_json_decode() { +async fn websocket_oversized_inbound_frame_triggers_reconnect_before_json_decode() { let (listener, url) = bind_local_listener().await; let server = tokio::spawn(async move { - let (stream, _) = listener.accept().await.expect("local server must accept client"); - let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed"); + let (stream, _) = listener.accept().await.expect("local server must accept initial client"); + let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("initial local WebSocket handshake must succeed"); websocket.send(tokio_tungstenite::tungstenite::Message::Text("X".repeat(512).into())).await.expect("oversized fixture message must send"); - tokio::time::sleep(std::time::Duration::from_millis(200)).await; + let (replacement_stream, _) = listener.accept().await.expect("local server must accept replacement client"); + let mut replacement = tokio_tungstenite::accept_async(replacement_stream).await.expect("replacement local WebSocket handshake must succeed"); + let _ = replacement.next().await; + let _ = replacement.flush().await; }); let settings = session_settings(std::time::Duration::from_secs(1), std::time::Duration::from_millis(200), 8, 128, 64, 1024); let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed"); - wait_for_state(&session, crate::WsSessionState::Failed).await; + wait_for_gap_count(&session, 1).await; + assert_eq!(session.state(), crate::WsSessionState::Active); + session.close().await.expect("recovered session must close cleanly"); server.await.expect("local server task must complete"); } diff --git a/deltas/0.2.7/pre.007-fix.001.md b/deltas/0.2.7/pre.007-fix.001.md new file mode 100644 index 0000000..3740752 --- /dev/null +++ b/deltas/0.2.7/pre.007-fix.001.md @@ -0,0 +1,139 @@ + + + +# Delta `0.2.7-pre.007-fix.001` — représentation reconnect compacte + canari oversized aligné + +## 1. Base requise + +```text +0.2.7-pre.007 appliqué +workspace.package.version = 0.2.7-pre.7 +``` + +Le checkpoint opérateur de `pre.007` a établi : + +```text +cargo fmt --all exécuté +python3 scripts/audit_rust_workspace_rules.py clean +cargo check --workspace réussi +cargo clippy --workspace --all-targets réussi avec 2 warnings large_enum_variant +cargo test -p ksp-onchain-transport-lib 282 réussis, 1 échoué +cargo test --workspace échoue sur le même canari Transport +``` + +Le seul test en échec est : + +```text +websocket_oversized_inbound_frame_fails_before_json_decode +``` + +## 2. Diagnostic du canari oversized + +Le canari provenait de `pre.005`, où une erreur de frame WebSocket conduisait directement la session physique à l'état terminal `Failed`. + +`pre.007` a volontairement changé ce contrat : une erreur physique, I/O, TLS, WebSocket ou protocolaire structurelle entre désormais dans le budget de reconnect. Une frame entrante dépassant `max_frame_size` est donc toujours rejetée par Tungstenite avant le décodage JSON, mais l'actor doit ensuite tenter la reconnexion au lieu de devenir immédiatement terminal. + +L'ancienne assertion : + +```text +oversized frame -> Failed +``` + +était devenue incompatible avec la politique runtime acquise par `pre.007`. + +## 3. Correctif du canari oversized + +Le fixture local accepte désormais deux connexions physiques : + +1. la connexion initiale envoie une frame texte de 512 octets alors que `max_frame_size = 64` ; +2. l'actor détecte la rupture provoquée par la limite Tungstenite et entre en reconnect ; +3. le serveur accepte la connexion de remplacement ; +4. le caller attend `continuity_gap_count == 1` et le retour à `WsSessionState::Active` ; +5. la session récupérée est fermée explicitement avec `WsSession::close()`. + +Le canari est renommé : + +```text +websocket_oversized_inbound_frame_triggers_reconnect_before_json_decode +``` + +Il vérifie ainsi simultanément que la limite est appliquée avant le parse JSON et que le nouveau lifecycle reconnect de `pre.007` est respecté. + +## 4. Warnings `large_enum_variant` + +Clippy signalait deux enums internes dont la variante `Connected` embarquait directement le type lourd `WsPhysicalStream` : + +```text +WsReconnectOutcome +WsConnectAttemptOutcome +``` + +Le socket est désormais stocké derrière `Box` uniquement dans ces objets de résultat transitoires, puis immédiatement déboxé lorsque l'actor reprend la propriété de la connexion. + +Ce changement : + +- réduit fortement la taille des enums ; +- supprime les warnings `clippy::large_enum_variant` ; +- ne change ni la propriété du socket, ni la cardinalité des sessions, ni la sémantique reconnect ; +- n'ajoute aucune allocation persistante autour du socket une fois celui-ci réinstallé dans l'actor. + +## 5. Signal technique + +Le fix modifie du Rust et des tests. Conformément aux règles KSP : + +```text +livraison = 0.2.7-pre.007-fix.001 +workspace.package.version = 0.2.7-pre.7.fix.1 +commit = v0.2.7-pre.007-fix.001 +``` + +Aucun tag prerelease. + +## 6. Fichiers modifiés + +```text +Cargo.toml +crates/ksp-onchain-transport-lib/src/ws_session.rs +crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs +``` + +## 7. Fichier ajouté + +```text +deltas/0.2.7/pre.007-fix.001.md +``` + +## 8. Fichiers supprimés + +Aucun. + +## 9. Invariants préservés + +Le fix ne change pas : + +- les APIs publiques `WsSession` et `WsSubscription` ; +- les IDs locaux et remote ; +- le mapping remote vers local ; +- la politique `ActiveSubscriptions` ou `Never` ; +- le budget, le calcul ou le reset du reconnect ; +- l'ordre déterministe des resubscriptions ; +- la priorité de l'unsubscribe local ; +- la sémantique des continuity gaps ; +- les frontières de dépendances ; +- la façade de logging KSP ; +- le scope de `pre.008` et des wrappers publics ultérieurs. + +## 10. Validation sandbox + +Le sandbox ne permet pas de revendiquer les gates Cargo opérateur. Les contrôles statiques disponibles sont exécutés sur le workspace reconstruit avec ce fix. + +## 11. Validation opérateur attendue + +```bash +cargo fmt --all +python3 scripts/audit_rust_workspace_rules.py +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-onchain-transport-lib +cargo test --workspace +```