|
|
|
|
@@ -1,5 +1,5 @@
|
|
|
|
|
// file: crates/ksp-onchain-transport-lib/unit_tests/ws_session.rs
|
|
|
|
|
// version: 4
|
|
|
|
|
// version: 5
|
|
|
|
|
|
|
|
|
|
use futures_util::SinkExt; // rust-rules: trait-import
|
|
|
|
|
use futures_util::StreamExt; // rust-rules: trait-import
|
|
|
|
|
@@ -44,6 +44,23 @@ fn session_settings(
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reconnect_session_settings(max_retries: u32, backoff: std::time::Duration, resubscribe: crate::WsResubscribePolicy) -> crate::WsSessionSettings {
|
|
|
|
|
let defaults = crate::WsSessionSettings::default();
|
|
|
|
|
return crate::WsSessionSettings::new(
|
|
|
|
|
std::time::Duration::from_millis(250),
|
|
|
|
|
std::time::Duration::from_millis(200),
|
|
|
|
|
crate::WsReconnectSettings::new(max_retries, backoff, backoff),
|
|
|
|
|
resubscribe,
|
|
|
|
|
defaults.command_queue_capacity(),
|
|
|
|
|
defaults.notification_queue_capacity(),
|
|
|
|
|
defaults.max_active_subscriptions(),
|
|
|
|
|
defaults.max_pending_requests(),
|
|
|
|
|
defaults.max_message_size_bytes(),
|
|
|
|
|
defaults.max_frame_size_bytes(),
|
|
|
|
|
defaults.max_write_buffer_size_bytes(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn bind_local_listener() -> (tokio::net::TcpListener, std::string::String) {
|
|
|
|
|
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.expect("local listener must bind");
|
|
|
|
|
let address = listener.local_addr().expect("local listener must expose address");
|
|
|
|
|
@@ -84,6 +101,17 @@ async fn wait_for_pending_count(session: &crate::WsSession, expected: usize) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn wait_for_gap_count(session: &crate::WsSession, expected: u64) {
|
|
|
|
|
let deadline = tokio::time::Instant::now() + std::time::Duration::from_secs(2);
|
|
|
|
|
loop {
|
|
|
|
|
if session.snapshot().continuity_gap_count() == expected && session.state() == crate::WsSessionState::Active {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
assert!(tokio::time::Instant::now() < deadline, "session did not recover with continuity gap count {expected}");
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(5)).await;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_session_connects_and_round_trips_internal_json_rpc() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
@@ -314,17 +342,17 @@ async fn websocket_ping_flushes_automatic_pong_and_keeps_session_active() {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_remote_close_transitions_to_closed_instead_of_failed() {
|
|
|
|
|
async fn websocket_remote_close_consumes_bounded_reconnect_budget_before_terminal_failure() {
|
|
|
|
|
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");
|
|
|
|
|
websocket.send(tokio_tungstenite::tungstenite::Message::Close(std::option::Option::None)).await.expect("fixture Close must send");
|
|
|
|
|
let _ = tokio::time::timeout(std::time::Duration::from_millis(200), websocket.next()).await;
|
|
|
|
|
});
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint(url.as_str())).await.expect("client handshake must succeed");
|
|
|
|
|
wait_for_state(&session, crate::WsSessionState::Closed).await;
|
|
|
|
|
assert!(session.close().await.is_ok());
|
|
|
|
|
let settings = reconnect_session_settings(1, std::time::Duration::from_millis(20), crate::WsResubscribePolicy::ActiveSubscriptions);
|
|
|
|
|
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;
|
|
|
|
|
assert_eq!(session.snapshot().continuity_gap_count(), 1);
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -381,6 +409,319 @@ async fn websocket_repeated_connect_close_cycles_are_bounded() {
|
|
|
|
|
.expect("repeated close server task must join");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_reconnect_resubscribes_in_local_id_order_and_remaps_remote_ids() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let (first_stream, _) = listener.accept().await.expect("initial client must connect");
|
|
|
|
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial handshake must succeed");
|
|
|
|
|
let account = read_request(&mut first).await;
|
|
|
|
|
assert_eq!(account.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("accountSubscribe"));
|
|
|
|
|
send_result(&mut first, &account, serde_json::json!(101)).await;
|
|
|
|
|
let logs = read_request(&mut first).await;
|
|
|
|
|
assert_eq!(logs.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("logsSubscribe"));
|
|
|
|
|
send_result(&mut first, &logs, serde_json::json!(202)).await;
|
|
|
|
|
drop(first);
|
|
|
|
|
let (second_stream, _) = listener.accept().await.expect("replacement client must connect");
|
|
|
|
|
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("replacement handshake must succeed");
|
|
|
|
|
let restored_account = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(restored_account.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("accountSubscribe"));
|
|
|
|
|
assert_eq!(restored_account.get("params"), account.get("params"));
|
|
|
|
|
send_result(&mut second, &restored_account, serde_json::json!(301)).await;
|
|
|
|
|
let restored_logs = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(restored_logs.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("logsSubscribe"));
|
|
|
|
|
assert_eq!(restored_logs.get("params"), logs.get("params"));
|
|
|
|
|
send_result(&mut second, &restored_logs, serde_json::json!(302)).await;
|
|
|
|
|
send_notification(&mut second, "logsNotification", 302, serde_json::json!({"generation": 2, "family": "logs"})).await;
|
|
|
|
|
send_notification(&mut second, "accountNotification", 301, serde_json::json!({"generation": 2, "family": "account"})).await;
|
|
|
|
|
let request = read_request(&mut second).await;
|
|
|
|
|
send_result(&mut second, &request, serde_json::json!(true)).await;
|
|
|
|
|
});
|
|
|
|
|
let settings = reconnect_session_settings(2, std::time::Duration::from_millis(20), crate::WsResubscribePolicy::ActiveSubscriptions);
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed");
|
|
|
|
|
let mut account = session
|
|
|
|
|
.subscribe_typed(crate::WsSubscriptionKind::Account, std::vec![serde_json::json!("account-canary")], |value| {
|
|
|
|
|
return std::result::Result::Ok(value);
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("account subscribe must succeed");
|
|
|
|
|
let mut logs = session
|
|
|
|
|
.subscribe_typed(crate::WsSubscriptionKind::Logs, std::vec![serde_json::json!({"mentions": ["log-canary"]})], |value| {
|
|
|
|
|
return std::result::Result::Ok(value);
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("logs subscribe must succeed");
|
|
|
|
|
let account_id = account.id();
|
|
|
|
|
let logs_id = logs.id();
|
|
|
|
|
let logs_value = tokio::time::timeout(std::time::Duration::from_secs(2), logs.recv())
|
|
|
|
|
.await
|
|
|
|
|
.expect("logs resubscribe notification must remain bounded")
|
|
|
|
|
.expect("logs channel must remain open")
|
|
|
|
|
.expect("logs notification must decode");
|
|
|
|
|
let account_value = tokio::time::timeout(std::time::Duration::from_secs(2), account.recv())
|
|
|
|
|
.await
|
|
|
|
|
.expect("account resubscribe notification must remain bounded")
|
|
|
|
|
.expect("account channel must remain open")
|
|
|
|
|
.expect("account notification must decode");
|
|
|
|
|
assert_eq!(logs_value, serde_json::json!({"generation": 2, "family": "logs"}));
|
|
|
|
|
assert_eq!(account_value, serde_json::json!({"generation": 2, "family": "account"}));
|
|
|
|
|
assert_eq!(account.id(), account_id);
|
|
|
|
|
assert_eq!(logs.id(), logs_id);
|
|
|
|
|
assert_eq!(account.state(), crate::WsSubscriptionState::Active);
|
|
|
|
|
assert_eq!(logs.state(), crate::WsSubscriptionState::Active);
|
|
|
|
|
assert_eq!(session.snapshot().continuity_gap_count(), 1);
|
|
|
|
|
assert!(session.snapshot().subscriptions().iter().all(|snapshot| return snapshot.remote_bound()));
|
|
|
|
|
let result = session.execute_json_rpc("afterResubscribe", std::vec::Vec::new()).await.expect("reconnected session must remain usable");
|
|
|
|
|
assert_eq!(result, serde_json::json!(true));
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_unsubscribe_during_reconnect_prevents_resubscribe() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
let (dropped_tx, dropped_rx) = tokio::sync::oneshot::channel();
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let (first_stream, _) = listener.accept().await.expect("initial client must connect");
|
|
|
|
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial handshake must succeed");
|
|
|
|
|
let subscribe = read_request(&mut first).await;
|
|
|
|
|
send_result(&mut first, &subscribe, serde_json::json!(41)).await;
|
|
|
|
|
drop(first);
|
|
|
|
|
let _ = dropped_tx.send(());
|
|
|
|
|
let (second_stream, _) = listener.accept().await.expect("replacement client must connect");
|
|
|
|
|
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("replacement handshake must succeed");
|
|
|
|
|
let first_request = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(first_request.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("afterReconnectCancel"));
|
|
|
|
|
send_result(&mut second, &first_request, serde_json::json!(true)).await;
|
|
|
|
|
});
|
|
|
|
|
let settings = reconnect_session_settings(2, std::time::Duration::from_millis(120), crate::WsResubscribePolicy::ActiveSubscriptions);
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed");
|
|
|
|
|
let mut subscription = session
|
|
|
|
|
.subscribe_typed(crate::WsSubscriptionKind::Root, std::vec::Vec::new(), |value| {
|
|
|
|
|
return std::result::Result::Ok(value);
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("root subscribe must succeed");
|
|
|
|
|
dropped_rx.await.expect("fixture must drop initial socket");
|
|
|
|
|
wait_for_state(&session, crate::WsSessionState::Reconnecting { attempt: 1 }).await;
|
|
|
|
|
assert!(!subscription.unsubscribe().await.expect("local cancellation during reconnect must succeed"));
|
|
|
|
|
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
|
|
|
|
|
wait_for_gap_count(&session, 1).await;
|
|
|
|
|
assert_eq!(session.snapshot().subscription_count(), 0);
|
|
|
|
|
let result = session
|
|
|
|
|
.execute_json_rpc("afterReconnectCancel", std::vec::Vec::new())
|
|
|
|
|
.await
|
|
|
|
|
.expect("session must remain usable after reconnect cancellation");
|
|
|
|
|
assert_eq!(result, serde_json::json!(true));
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_late_resubscribe_ack_after_unsubscribe_is_cleaned_up_without_reactivation() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
let (resubscribe_seen_tx, resubscribe_seen_rx) = tokio::sync::oneshot::channel();
|
|
|
|
|
let (release_ack_tx, release_ack_rx) = tokio::sync::oneshot::channel();
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let (first_stream, _) = listener.accept().await.expect("initial client must connect");
|
|
|
|
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial handshake must succeed");
|
|
|
|
|
let subscribe = read_request(&mut first).await;
|
|
|
|
|
send_result(&mut first, &subscribe, serde_json::json!(41)).await;
|
|
|
|
|
drop(first);
|
|
|
|
|
let (second_stream, _) = listener.accept().await.expect("replacement client must connect");
|
|
|
|
|
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("replacement handshake must succeed");
|
|
|
|
|
let resubscribe = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(resubscribe.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("rootSubscribe"));
|
|
|
|
|
let _ = resubscribe_seen_tx.send(());
|
|
|
|
|
release_ack_rx.await.expect("test must release stale acknowledgement");
|
|
|
|
|
send_result(&mut second, &resubscribe, serde_json::json!(99)).await;
|
|
|
|
|
let cleanup = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(cleanup.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("rootUnsubscribe"));
|
|
|
|
|
assert_eq!(cleanup.get("params"), std::option::Option::Some(&serde_json::json!([99])));
|
|
|
|
|
send_result(&mut second, &cleanup, serde_json::json!(true)).await;
|
|
|
|
|
let request = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(request.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("afterStaleAck"));
|
|
|
|
|
send_result(&mut second, &request, serde_json::json!(true)).await;
|
|
|
|
|
});
|
|
|
|
|
let settings = reconnect_session_settings(2, std::time::Duration::from_millis(20), crate::WsResubscribePolicy::ActiveSubscriptions);
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed");
|
|
|
|
|
let mut subscription = session
|
|
|
|
|
.subscribe_typed(crate::WsSubscriptionKind::Root, std::vec::Vec::new(), |value| {
|
|
|
|
|
return std::result::Result::Ok(value);
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("root subscribe must succeed");
|
|
|
|
|
resubscribe_seen_rx.await.expect("fixture must observe replacement subscribe");
|
|
|
|
|
assert!(!subscription.unsubscribe().await.expect("local cancellation must win while resubscribe acknowledgement is pending"));
|
|
|
|
|
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
|
|
|
|
|
release_ack_tx.send(()).expect("fixture stale acknowledgement must be released");
|
|
|
|
|
wait_for_gap_count(&session, 1).await;
|
|
|
|
|
assert_eq!(subscription.state(), crate::WsSubscriptionState::Closed);
|
|
|
|
|
assert_eq!(session.snapshot().subscription_count(), 0);
|
|
|
|
|
let result = session.execute_json_rpc("afterStaleAck", std::vec::Vec::new()).await.expect("session must remain usable after stale ack cleanup");
|
|
|
|
|
assert_eq!(result, serde_json::json!(true));
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_reconnect_budget_resets_after_full_active_recovery() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let (first_stream, _) = listener.accept().await.expect("initial client must connect");
|
|
|
|
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial handshake must succeed");
|
|
|
|
|
let first_request = read_request(&mut first).await;
|
|
|
|
|
send_result(&mut first, &first_request, serde_json::json!(0)).await;
|
|
|
|
|
drop(first);
|
|
|
|
|
let (second_stream, _) = listener.accept().await.expect("first replacement must connect");
|
|
|
|
|
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("first replacement handshake must succeed");
|
|
|
|
|
let second_request = read_request(&mut second).await;
|
|
|
|
|
send_result(&mut second, &second_request, serde_json::json!(1)).await;
|
|
|
|
|
drop(second);
|
|
|
|
|
let (third_stream, _) = listener.accept().await.expect("second replacement must connect");
|
|
|
|
|
let mut third = tokio_tungstenite::accept_async(third_stream).await.expect("second replacement handshake must succeed");
|
|
|
|
|
let third_request = read_request(&mut third).await;
|
|
|
|
|
send_result(&mut third, &third_request, serde_json::json!(2)).await;
|
|
|
|
|
});
|
|
|
|
|
let settings = reconnect_session_settings(1, std::time::Duration::from_millis(20), crate::WsResubscribePolicy::ActiveSubscriptions);
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed");
|
|
|
|
|
let first = session.execute_json_rpc("cycleZero", std::vec::Vec::new()).await.expect("initial request must succeed");
|
|
|
|
|
assert_eq!(first, serde_json::json!(0));
|
|
|
|
|
wait_for_gap_count(&session, 1).await;
|
|
|
|
|
let second = session.execute_json_rpc("cycleOne", std::vec::Vec::new()).await.expect("first recovered request must succeed");
|
|
|
|
|
assert_eq!(second, serde_json::json!(1));
|
|
|
|
|
wait_for_gap_count(&session, 2).await;
|
|
|
|
|
let third = session.execute_json_rpc("cycleTwo", std::vec::Vec::new()).await.expect("second recovered request must succeed");
|
|
|
|
|
assert_eq!(third, serde_json::json!(2));
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_resubscribe_never_keeps_session_but_fails_logical_subscription() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let (first_stream, _) = listener.accept().await.expect("initial client must connect");
|
|
|
|
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial handshake must succeed");
|
|
|
|
|
let subscribe = read_request(&mut first).await;
|
|
|
|
|
send_result(&mut first, &subscribe, serde_json::json!(71)).await;
|
|
|
|
|
drop(first);
|
|
|
|
|
let (second_stream, _) = listener.accept().await.expect("replacement client must connect");
|
|
|
|
|
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("replacement handshake must succeed");
|
|
|
|
|
let request = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(request.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("afterNeverPolicy"));
|
|
|
|
|
send_result(&mut second, &request, serde_json::json!(true)).await;
|
|
|
|
|
});
|
|
|
|
|
let settings = reconnect_session_settings(2, std::time::Duration::from_millis(20), crate::WsResubscribePolicy::Never);
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed");
|
|
|
|
|
let subscription = session
|
|
|
|
|
.subscribe_typed(crate::WsSubscriptionKind::Root, std::vec::Vec::new(), |value| {
|
|
|
|
|
return std::result::Result::Ok(value);
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("root subscribe must succeed");
|
|
|
|
|
wait_for_gap_count(&session, 1).await;
|
|
|
|
|
assert_eq!(subscription.state(), crate::WsSubscriptionState::Failed);
|
|
|
|
|
assert_eq!(session.snapshot().subscription_count(), 0);
|
|
|
|
|
let result = session.execute_json_rpc("afterNeverPolicy", std::vec::Vec::new()).await.expect("physical session must reconnect without resubscribe");
|
|
|
|
|
assert_eq!(result, serde_json::json!(true));
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn websocket_reconnect_backoff_is_exponential_and_bounded() {
|
|
|
|
|
let settings = crate::WsReconnectSettings::new(5, std::time::Duration::from_millis(10), std::time::Duration::from_millis(40));
|
|
|
|
|
assert_eq!(super::reconnect_backoff(&settings, 1), std::time::Duration::from_millis(10));
|
|
|
|
|
assert_eq!(super::reconnect_backoff(&settings, 2), std::time::Duration::from_millis(20));
|
|
|
|
|
assert_eq!(super::reconnect_backoff(&settings, 3), std::time::Duration::from_millis(40));
|
|
|
|
|
assert_eq!(super::reconnect_backoff(&settings, 4), std::time::Duration::from_millis(40));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_shutdown_interrupts_reconnect_backoff_without_new_connection() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
let (dropped_tx, dropped_rx) = tokio::sync::oneshot::channel();
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let (stream, _) = listener.accept().await.expect("initial client must connect");
|
|
|
|
|
let websocket = tokio_tungstenite::accept_async(stream).await.expect("initial handshake must succeed");
|
|
|
|
|
drop(websocket);
|
|
|
|
|
let _ = dropped_tx.send(());
|
|
|
|
|
tokio::time::sleep(std::time::Duration::from_millis(500)).await;
|
|
|
|
|
});
|
|
|
|
|
let defaults = crate::WsSessionSettings::default();
|
|
|
|
|
let settings = crate::WsSessionSettings::new(
|
|
|
|
|
std::time::Duration::from_millis(250),
|
|
|
|
|
std::time::Duration::from_millis(150),
|
|
|
|
|
crate::WsReconnectSettings::new(5, std::time::Duration::from_secs(2), std::time::Duration::from_secs(2)),
|
|
|
|
|
crate::WsResubscribePolicy::ActiveSubscriptions,
|
|
|
|
|
defaults.command_queue_capacity(),
|
|
|
|
|
defaults.notification_queue_capacity(),
|
|
|
|
|
defaults.max_active_subscriptions(),
|
|
|
|
|
defaults.max_pending_requests(),
|
|
|
|
|
defaults.max_message_size_bytes(),
|
|
|
|
|
defaults.max_frame_size_bytes(),
|
|
|
|
|
defaults.max_write_buffer_size_bytes(),
|
|
|
|
|
);
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed");
|
|
|
|
|
dropped_rx.await.expect("fixture must drop physical connection");
|
|
|
|
|
wait_for_state(&session, crate::WsSessionState::Reconnecting { attempt: 1 }).await;
|
|
|
|
|
tokio::time::timeout(std::time::Duration::from_millis(300), session.close())
|
|
|
|
|
.await
|
|
|
|
|
.expect("shutdown must interrupt reconnect backoff")
|
|
|
|
|
.expect("shutdown during reconnect must finish cleanly");
|
|
|
|
|
assert_eq!(session.state(), crate::WsSessionState::Closed);
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[tokio::test(flavor = "current_thread")]
|
|
|
|
|
async fn websocket_resubscribe_application_error_fails_only_one_subscription() {
|
|
|
|
|
let (listener, url) = bind_local_listener().await;
|
|
|
|
|
let server = tokio::spawn(async move {
|
|
|
|
|
let (first_stream, _) = listener.accept().await.expect("initial client must connect");
|
|
|
|
|
let mut first = tokio_tungstenite::accept_async(first_stream).await.expect("initial handshake must succeed");
|
|
|
|
|
let account = read_request(&mut first).await;
|
|
|
|
|
send_result(&mut first, &account, serde_json::json!(11)).await;
|
|
|
|
|
let logs = read_request(&mut first).await;
|
|
|
|
|
send_result(&mut first, &logs, serde_json::json!(12)).await;
|
|
|
|
|
drop(first);
|
|
|
|
|
let (second_stream, _) = listener.accept().await.expect("replacement client must connect");
|
|
|
|
|
let mut second = tokio_tungstenite::accept_async(second_stream).await.expect("replacement handshake must succeed");
|
|
|
|
|
let failed = read_request(&mut second).await;
|
|
|
|
|
let failed_id = failed.get("id").and_then(serde_json::Value::as_u64).expect("resubscribe id must be numeric");
|
|
|
|
|
let error = serde_json::json!({"jsonrpc":"2.0","id":failed_id,"error":{"code":-32602,"message":"fixture resubscribe rejection"}});
|
|
|
|
|
second.send(tokio_tungstenite::tungstenite::Message::Text(error.to_string().into())).await.expect("resubscribe application error must send");
|
|
|
|
|
let restored = read_request(&mut second).await;
|
|
|
|
|
assert_eq!(restored.get("method").and_then(serde_json::Value::as_str), std::option::Option::Some("logsSubscribe"));
|
|
|
|
|
send_result(&mut second, &restored, serde_json::json!(88)).await;
|
|
|
|
|
send_notification(&mut second, "logsNotification", 88, serde_json::json!({"restored": true})).await;
|
|
|
|
|
let request = read_request(&mut second).await;
|
|
|
|
|
send_result(&mut second, &request, serde_json::json!(true)).await;
|
|
|
|
|
});
|
|
|
|
|
let settings = reconnect_session_settings(2, std::time::Duration::from_millis(20), crate::WsResubscribePolicy::ActiveSubscriptions);
|
|
|
|
|
let session = crate::WsSession::connect(local_endpoint_with_session(url.as_str(), settings)).await.expect("client handshake must succeed");
|
|
|
|
|
let account = session
|
|
|
|
|
.subscribe_typed(crate::WsSubscriptionKind::Account, std::vec![serde_json::json!("account")], |value| {
|
|
|
|
|
return std::result::Result::Ok(value);
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("account subscribe must succeed");
|
|
|
|
|
let mut logs = session
|
|
|
|
|
.subscribe_typed(crate::WsSubscriptionKind::Logs, std::vec![serde_json::json!("all")], |value| {
|
|
|
|
|
return std::result::Result::Ok(value);
|
|
|
|
|
})
|
|
|
|
|
.await
|
|
|
|
|
.expect("logs subscribe must succeed");
|
|
|
|
|
let notification = tokio::time::timeout(std::time::Duration::from_secs(2), logs.recv())
|
|
|
|
|
.await
|
|
|
|
|
.expect("restored logs notification must remain bounded")
|
|
|
|
|
.expect("logs channel must remain open")
|
|
|
|
|
.expect("logs notification must decode");
|
|
|
|
|
assert_eq!(notification, serde_json::json!({"restored": true}));
|
|
|
|
|
assert_eq!(account.state(), crate::WsSubscriptionState::Failed);
|
|
|
|
|
assert_eq!(logs.state(), crate::WsSubscriptionState::Active);
|
|
|
|
|
assert_eq!(session.state(), crate::WsSessionState::Active);
|
|
|
|
|
assert_eq!(session.snapshot().continuity_gap_count(), 1);
|
|
|
|
|
let result = session.execute_json_rpc("afterPartialResubscribe", std::vec::Vec::new()).await.expect("session must remain usable");
|
|
|
|
|
assert_eq!(result, serde_json::json!(true));
|
|
|
|
|
server.await.expect("local server task must complete");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn send_notification(
|
|
|
|
|
websocket: &mut tokio_tungstenite::WebSocketStream<tokio::net::TcpStream>,
|
|
|
|
|
method: &str,
|
|
|
|
|
|