v0.2.7-pre.007-fix.001
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 201
|
# version: 202
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
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"]
|
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]
|
[workspace.package]
|
||||||
version = "0.2.7-pre.7"
|
version = "0.2.7-pre.7.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/src/ws_session.rs
|
// 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::SinkExt; // rust-rules: trait-import
|
||||||
use futures_util::StreamExt; // rust-rules: trait-import
|
use futures_util::StreamExt; // rust-rules: trait-import
|
||||||
@@ -280,7 +280,7 @@ enum WsActorIoOutcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum WsReconnectOutcome {
|
enum WsReconnectOutcome {
|
||||||
Connected { websocket: WsPhysicalStream },
|
Connected { websocket: std::boxed::Box<WsPhysicalStream> },
|
||||||
ShutdownRequested { deadline: tokio::time::Instant },
|
ShutdownRequested { deadline: tokio::time::Instant },
|
||||||
HandlesDropped,
|
HandlesDropped,
|
||||||
Exhausted,
|
Exhausted,
|
||||||
@@ -293,7 +293,7 @@ enum WsReconnectControlOutcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum WsConnectAttemptOutcome {
|
enum WsConnectAttemptOutcome {
|
||||||
Connected { websocket: WsPhysicalStream, handshake_status: u16 },
|
Connected { websocket: std::boxed::Box<WsPhysicalStream>, handshake_status: u16 },
|
||||||
Retry,
|
Retry,
|
||||||
ShutdownRequested { deadline: tokio::time::Instant },
|
ShutdownRequested { deadline: tokio::time::Instant },
|
||||||
HandlesDropped,
|
HandlesDropped,
|
||||||
@@ -481,7 +481,7 @@ async fn run_ws_session_actor(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
match recovery {
|
match recovery {
|
||||||
WsReconnectOutcome::Connected { websocket: replacement } => websocket = replacement,
|
WsReconnectOutcome::Connected { websocket: replacement } => websocket = *replacement,
|
||||||
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
||||||
finish_disconnected_shutdown(
|
finish_disconnected_shutdown(
|
||||||
id,
|
id,
|
||||||
@@ -522,7 +522,7 @@ async fn run_ws_session_actor(
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
match recovery {
|
match recovery {
|
||||||
WsReconnectOutcome::Connected { websocket: replacement } => websocket = replacement,
|
WsReconnectOutcome::Connected { websocket: replacement } => websocket = *replacement,
|
||||||
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
WsReconnectOutcome::ShutdownRequested { deadline } => {
|
||||||
finish_disconnected_shutdown(
|
finish_disconnected_shutdown(
|
||||||
id,
|
id,
|
||||||
@@ -610,7 +610,7 @@ async fn recover_websocket_session(
|
|||||||
handshake_status,
|
handshake_status,
|
||||||
"replacement physical WebSocket connection established"
|
"replacement physical WebSocket connection established"
|
||||||
);
|
);
|
||||||
websocket
|
*websocket
|
||||||
},
|
},
|
||||||
WsConnectAttemptOutcome::Retry => {
|
WsConnectAttemptOutcome::Retry => {
|
||||||
attempt = attempt.saturating_add(1);
|
attempt = attempt.saturating_add(1);
|
||||||
@@ -645,7 +645,7 @@ async fn recover_websocket_session(
|
|||||||
continuity_gap_count = *continuity_gap_count,
|
continuity_gap_count = *continuity_gap_count,
|
||||||
"physical WebSocket reconnect completed and retry budget reset"
|
"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::ShutdownRequested { deadline } => return WsReconnectOutcome::ShutdownRequested { deadline },
|
||||||
WsActorIoOutcome::RemoteClosed => {
|
WsActorIoOutcome::RemoteClosed => {
|
||||||
@@ -872,7 +872,7 @@ async fn connect_replacement_websocket(
|
|||||||
result = &mut connect => {
|
result = &mut connect => {
|
||||||
return match result {
|
return match result {
|
||||||
std::result::Result::Ok((websocket, response)) => WsConnectAttemptOutcome::Connected {
|
std::result::Result::Ok((websocket, response)) => WsConnectAttemptOutcome::Connected {
|
||||||
websocket,
|
websocket: std::boxed::Box::new(websocket),
|
||||||
handshake_status: response.status().as_u16(),
|
handshake_status: response.status().as_u16(),
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => {
|
std::result::Result::Err(_) => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
// 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::SinkExt; // rust-rules: trait-import
|
||||||
use futures_util::StreamExt; // 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")]
|
#[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 (listener, url) = bind_local_listener().await;
|
||||||
let server = tokio::spawn(async move {
|
let server = tokio::spawn(async move {
|
||||||
let (stream, _) = listener.accept().await.expect("local server must accept client");
|
let (stream, _) = listener.accept().await.expect("local server must accept initial client");
|
||||||
let mut websocket = tokio_tungstenite::accept_async(stream).await.expect("local WebSocket handshake must succeed");
|
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");
|
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 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");
|
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");
|
server.await.expect("local server task must complete");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
139
deltas/0.2.7/pre.007-fix.001.md
Normal file
139
deltas/0.2.7/pre.007-fix.001.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<!-- file: deltas/0.2.7/pre.007-fix.001.md -->
|
||||||
|
<!-- version: 1 -->
|
||||||
|
|
||||||
|
# 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<WsPhysicalStream>` 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<T>` ;
|
||||||
|
- 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
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user