mirror of
https://github.com/telemt/telemt.git
synced 2026-10-10 19:35:58 +03:00
WEB Manager Session + Generation Fence tests
This commit is contained in:
+3
-104
@@ -2,7 +2,6 @@ use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use bytes::{BufMut, Bytes, BytesMut};
|
||||
use tokio::sync::OwnedSemaphorePermit;
|
||||
|
||||
use super::lane_downlink::take_lane_down_batch;
|
||||
use super::{
|
||||
@@ -13,6 +12,9 @@ use crate::web::frame::{self, FrameType};
|
||||
use crate::web::manager::ManagerError;
|
||||
use crate::web::telemetry::WebSessionLifecycleObservation;
|
||||
|
||||
// Bounded lane-open admission and cancellation-safe wait lifecycle.
|
||||
mod lane_open_wait;
|
||||
|
||||
impl WebSession {
|
||||
/// Polls one lane with independent cursor replay and newest-poll-wins semantics.
|
||||
pub(crate) async fn poll_down_lane(
|
||||
@@ -311,97 +313,6 @@ impl WebSession {
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_lane_open(&self, lane_id: u32, cursor: u64) -> Result<bool, ManagerError> {
|
||||
if self.close_if_cancelled() {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
let wait = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed || self.cancel.is_cancelled() {
|
||||
drop(state);
|
||||
self.close_if_cancelled();
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
if state.carrier_lanes.contains_key(&lane_id) {
|
||||
return Ok(true);
|
||||
}
|
||||
if cursor != 0 || lane_id == 0 {
|
||||
drop(state);
|
||||
self.close(SessionCloseReason::Protocol);
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
if state.lane_open_waits >= self.limits.max_lane_open_waits_per_session {
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
let Some(manager) = self.manager.upgrade() else {
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
let Some(auxiliary) = manager.try_lane_poll(true) else {
|
||||
return Err(ManagerError::Limit);
|
||||
};
|
||||
state.lane_open_waits += 1;
|
||||
let observation = WebSessionLifecycleObservation::HttpActivityAfterGap;
|
||||
self.touch_peer_locked(&mut state, Instant::now(), observation);
|
||||
LaneOpenWaitGuard {
|
||||
session: self,
|
||||
_auxiliary: auxiliary,
|
||||
}
|
||||
};
|
||||
let deadline = Duration::from_secs(self.timeouts.lane_open_wait_secs);
|
||||
let opened = tokio::time::timeout(deadline, async {
|
||||
loop {
|
||||
let notified = self.lane_open_notify.notified();
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
{
|
||||
let state = self.state.lock();
|
||||
if state.closed || self.cancel.is_cancelled() {
|
||||
drop(state);
|
||||
self.close_if_cancelled();
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
if state.carrier_lanes.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
});
|
||||
let opened = tokio::select! {
|
||||
biased;
|
||||
_ = self.cancel.cancelled() => {
|
||||
drop(wait);
|
||||
self.close_if_cancelled();
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
opened = opened => opened,
|
||||
};
|
||||
drop(wait);
|
||||
match opened {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
let state = self.state.lock();
|
||||
if state.closed || self.cancel.is_cancelled() {
|
||||
drop(state);
|
||||
self.close_if_cancelled();
|
||||
Err(ManagerError::Closed)
|
||||
} else {
|
||||
Ok(state.carrier_lanes.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn queue_lane_frame_locked(
|
||||
&self,
|
||||
state: &mut SessionState,
|
||||
@@ -571,18 +482,6 @@ impl WebSession {
|
||||
}
|
||||
}
|
||||
|
||||
struct LaneOpenWaitGuard<'a> {
|
||||
session: &'a WebSession,
|
||||
_auxiliary: OwnedSemaphorePermit,
|
||||
}
|
||||
|
||||
impl Drop for LaneOpenWaitGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.session.state.lock();
|
||||
state.lane_open_waits = state.lane_open_waits.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Lane-specific protocol, replay, and lifecycle tests.
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use tokio::sync::OwnedSemaphorePermit;
|
||||
|
||||
use super::WebSession;
|
||||
use crate::web::manager::ManagerError;
|
||||
use crate::web::session::SessionCloseReason;
|
||||
use crate::web::telemetry::WebSessionLifecycleObservation;
|
||||
|
||||
impl WebSession {
|
||||
/// Waits for a non-control lane to become observable within the bounded admission budget.
|
||||
pub(super) async fn wait_for_lane_open(
|
||||
&self,
|
||||
lane_id: u32,
|
||||
cursor: u64,
|
||||
) -> Result<bool, ManagerError> {
|
||||
if self.close_if_cancelled() {
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
let wait = {
|
||||
let mut state = self.state.lock();
|
||||
if state.closed || self.cancel.is_cancelled() {
|
||||
drop(state);
|
||||
self.close_if_cancelled();
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
if state.carrier_lanes.contains_key(&lane_id) {
|
||||
return Ok(true);
|
||||
}
|
||||
if cursor != 0 || lane_id == 0 {
|
||||
drop(state);
|
||||
self.close(SessionCloseReason::Protocol);
|
||||
return Err(ManagerError::Protocol);
|
||||
}
|
||||
if state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
if state.lane_open_waits >= self.limits.max_lane_open_waits_per_session {
|
||||
return Err(ManagerError::Limit);
|
||||
}
|
||||
let Some(manager) = self.manager.upgrade() else {
|
||||
return Err(ManagerError::Closed);
|
||||
};
|
||||
let Some(auxiliary) = manager.try_lane_poll(true) else {
|
||||
return Err(ManagerError::Limit);
|
||||
};
|
||||
state.lane_open_waits += 1;
|
||||
let observation = WebSessionLifecycleObservation::HttpActivityAfterGap;
|
||||
self.touch_peer_locked(&mut state, Instant::now(), observation);
|
||||
LaneOpenWaitGuard {
|
||||
session: self,
|
||||
_auxiliary: auxiliary,
|
||||
}
|
||||
};
|
||||
let deadline = Duration::from_secs(self.timeouts.lane_open_wait_secs);
|
||||
let opened = tokio::time::timeout(deadline, async {
|
||||
loop {
|
||||
let notified = self.lane_open_notify.notified();
|
||||
tokio::pin!(notified);
|
||||
notified.as_mut().enable();
|
||||
{
|
||||
let state = self.state.lock();
|
||||
if state.closed || self.cancel.is_cancelled() {
|
||||
drop(state);
|
||||
self.close_if_cancelled();
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
if state.carrier_lanes.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id)
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
notified.await;
|
||||
}
|
||||
});
|
||||
let opened = tokio::select! {
|
||||
biased;
|
||||
_ = self.cancel.cancelled() => {
|
||||
drop(wait);
|
||||
self.close_if_cancelled();
|
||||
return Err(ManagerError::Closed);
|
||||
}
|
||||
opened = opened => opened,
|
||||
};
|
||||
drop(wait);
|
||||
match opened {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
let state = self.state.lock();
|
||||
if state.closed || self.cancel.is_cancelled() {
|
||||
drop(state);
|
||||
self.close_if_cancelled();
|
||||
Err(ManagerError::Closed)
|
||||
} else {
|
||||
Ok(state.carrier_lanes.contains_key(&lane_id)
|
||||
|| state.closed_streams.contains(&lane_id)
|
||||
|| state.closing_streams.contains_key(&lane_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LaneOpenWaitGuard<'a> {
|
||||
session: &'a WebSession,
|
||||
_auxiliary: OwnedSemaphorePermit,
|
||||
}
|
||||
|
||||
impl Drop for LaneOpenWaitGuard<'_> {
|
||||
fn drop(&mut self) {
|
||||
let mut state = self.session.state.lock();
|
||||
state.lane_open_waits = state.lane_open_waits.saturating_sub(1);
|
||||
}
|
||||
}
|
||||
@@ -299,258 +299,6 @@ impl WebSession {
|
||||
}
|
||||
}
|
||||
|
||||
// Carrier health publication and negotiation race tests.
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Barrier};
|
||||
|
||||
use super::*;
|
||||
use crate::config::{
|
||||
WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
|
||||
};
|
||||
use crate::web::manager::{CarrierClientClass, WebProcessRuntime};
|
||||
use crate::web::session::{SessionCloseOutcome, SessionCloseReason};
|
||||
|
||||
fn session(carrier: WebCarrier, deadline: Instant) -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
|
||||
user: "alice".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier,
|
||||
carrier_negotiation_enabled: true,
|
||||
carrier_learning: false,
|
||||
carriers: Arc::from([carrier]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [0; 32],
|
||||
credential_id: [0; 16],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 1,
|
||||
max_streams: 1,
|
||||
max_streams_per_session: 1,
|
||||
});
|
||||
WebSession::new(
|
||||
std::sync::Weak::<WebProcessRuntime>::new(),
|
||||
[1; 32],
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
carrier,
|
||||
1,
|
||||
[3; 32],
|
||||
Some(deadline),
|
||||
CarrierClientClass::Bridge,
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn arm_http_health(session: &WebSession, now: Instant) {
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_deadline_refuses_uncommitted_progress() {
|
||||
let session = session(WebCarrier::Https, Instant::now() - Duration::from_secs(1));
|
||||
let state = session.state.lock();
|
||||
assert_eq!(
|
||||
session.ensure_carrier_active_locked(&state),
|
||||
Err(crate::web::manager::ManagerError::Closed)
|
||||
);
|
||||
assert!(matches!(
|
||||
state.negotiation_phase,
|
||||
SessionNegotiationPhase::Uncommitted
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_health_requires_authenticated_activity_after_the_window() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now - Duration::from_secs(2));
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_health_requires_the_exact_live_probe_owner() {
|
||||
let session = session(
|
||||
WebCarrier::Websocket,
|
||||
Instant::now() + Duration::from_secs(60),
|
||||
);
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.websocket_carrier_active = true;
|
||||
state.websocket_commit_ack_owner = Some(7);
|
||||
state.websocket_commit_ack_written = true;
|
||||
state.carrier_health_uplink = true;
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
state.websocket_probe_claimed = true;
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_waits_for_manager_commit_publication() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Awaiting
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_publication_claim_is_single_shot() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
arm_http_health(&session, now);
|
||||
let mut state = session.state.lock();
|
||||
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
drop(state);
|
||||
assert_eq!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Publishing
|
||||
);
|
||||
assert!(session.publish_carrier_health());
|
||||
assert!(!session.publish_carrier_health());
|
||||
assert_eq!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Published
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_health_and_close_always_reach_one_terminal_state() {
|
||||
for _ in 0..512 {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
arm_http_health(&session, now);
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
let health_session = Arc::clone(&session);
|
||||
let health_barrier = Arc::clone(&barrier);
|
||||
let health = std::thread::spawn(move || {
|
||||
health_barrier.wait();
|
||||
std::thread::yield_now();
|
||||
let claim = {
|
||||
let mut state = health_session.state.lock();
|
||||
health_session.carrier_health_ready_locked(&mut state, now)
|
||||
};
|
||||
if claim.is_some() {
|
||||
health_session.publish_carrier_health();
|
||||
}
|
||||
});
|
||||
let close_session = Arc::clone(&session);
|
||||
let close_barrier = Arc::clone(&barrier);
|
||||
let close = std::thread::spawn(move || {
|
||||
close_barrier.wait();
|
||||
std::thread::yield_now();
|
||||
close_session.close(SessionCloseReason::ApiClose);
|
||||
});
|
||||
barrier.wait();
|
||||
health.join().unwrap();
|
||||
close.join().unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Published | CarrierHealthPublicationState::Rejected
|
||||
));
|
||||
assert!(!session.publish_carrier_health());
|
||||
assert_eq!(
|
||||
session.close(SessionCloseReason::ApiClose),
|
||||
SessionCloseOutcome::AlreadyClosing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_and_supersede_have_one_session_lock_winner() {
|
||||
let committed = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
{
|
||||
let mut state = committed.state.lock();
|
||||
assert!(
|
||||
committed
|
||||
.record_uplink_progress_locked(
|
||||
&mut state,
|
||||
AppliedProgress {
|
||||
accepted_open: true,
|
||||
accepted_data: true,
|
||||
},
|
||||
)
|
||||
.0
|
||||
);
|
||||
}
|
||||
assert!(!committed.begin_carrier_supersede());
|
||||
|
||||
let replacing = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
assert!(replacing.begin_carrier_supersede());
|
||||
assert_eq!(
|
||||
replacing.ensure_carrier_active_locked(&replacing.state.lock()),
|
||||
Err(crate::web::manager::ManagerError::Closed)
|
||||
);
|
||||
replacing.cancel_carrier_supersede();
|
||||
assert!(
|
||||
replacing
|
||||
.ensure_carrier_active_locked(&replacing.state.lock())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
mod tests;
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Barrier};
|
||||
|
||||
use super::*;
|
||||
use crate::config::{
|
||||
WebCarrier, WebLimitsConfig, WebRuntimeProfile, WebSecretMode, WebTimeoutsConfig,
|
||||
};
|
||||
use crate::web::manager::{CarrierClientClass, WebProcessRuntime};
|
||||
use crate::web::session::{SessionCloseOutcome, SessionCloseReason};
|
||||
|
||||
fn session(carrier: WebCarrier, deadline: Instant) -> Arc<WebSession> {
|
||||
let profile = Arc::new(WebRuntimeProfile {
|
||||
host: "proxy.example.com".to_string(),
|
||||
public_addr: SocketAddr::from(([203, 0, 113, 10], 443)),
|
||||
user: "alice".to_string(),
|
||||
secret_mode: WebSecretMode::Plain,
|
||||
carrier,
|
||||
carrier_negotiation_enabled: true,
|
||||
carrier_learning: false,
|
||||
carriers: Arc::from([carrier]),
|
||||
carrier_negotiation_deadlines_secs: [3, 5, 8, 12],
|
||||
capability: [0; 32],
|
||||
credential_id: [0; 16],
|
||||
key_fingerprint: "0000000000000000".to_string(),
|
||||
max_sessions: 1,
|
||||
max_streams: 1,
|
||||
max_streams_per_session: 1,
|
||||
});
|
||||
WebSession::new(
|
||||
std::sync::Weak::<WebProcessRuntime>::new(),
|
||||
[1; 32],
|
||||
"192.0.2.10".parse().unwrap(),
|
||||
1,
|
||||
profile,
|
||||
[2; 32],
|
||||
carrier,
|
||||
1,
|
||||
[3; 32],
|
||||
Some(deadline),
|
||||
CarrierClientClass::Bridge,
|
||||
None,
|
||||
true,
|
||||
false,
|
||||
WebLimitsConfig::default(),
|
||||
WebTimeoutsConfig::default(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
fn arm_http_health(session: &WebSession, now: Instant) {
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn final_deadline_refuses_uncommitted_progress() {
|
||||
let session = session(WebCarrier::Https, Instant::now() - Duration::from_secs(1));
|
||||
let state = session.state.lock();
|
||||
assert_eq!(
|
||||
session.ensure_carrier_active_locked(&state),
|
||||
Err(crate::web::manager::ManagerError::Closed)
|
||||
);
|
||||
assert!(matches!(
|
||||
state.negotiation_phase,
|
||||
SessionNegotiationPhase::Uncommitted
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn http_health_requires_authenticated_activity_after_the_window() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now - Duration::from_secs(2));
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn websocket_health_requires_the_exact_live_probe_owner() {
|
||||
let session = session(
|
||||
WebCarrier::Websocket,
|
||||
Instant::now() + Duration::from_secs(60),
|
||||
);
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_commit_published = true;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.websocket_carrier_active = true;
|
||||
state.websocket_commit_ack_owner = Some(7);
|
||||
state.websocket_commit_ack_written = true;
|
||||
state.carrier_health_uplink = true;
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
state.websocket_probe_claimed = true;
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_some()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_waits_for_manager_commit_publication() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
let mut state = session.state.lock();
|
||||
state.negotiation_phase = SessionNegotiationPhase::Committed;
|
||||
state.carrier_health_due_at = Some(now - Duration::from_secs(1));
|
||||
state.carrier_health_uplink = true;
|
||||
state.carrier_health_downlink = true;
|
||||
state.carrier_health_activity_at = Some(now);
|
||||
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
assert_eq!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Awaiting
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn health_publication_claim_is_single_shot() {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
arm_http_health(&session, now);
|
||||
let mut state = session.state.lock();
|
||||
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_some()
|
||||
);
|
||||
assert!(
|
||||
session
|
||||
.carrier_health_ready_locked(&mut state, now)
|
||||
.is_none()
|
||||
);
|
||||
drop(state);
|
||||
assert_eq!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Publishing
|
||||
);
|
||||
assert!(session.publish_carrier_health());
|
||||
assert!(!session.publish_carrier_health());
|
||||
assert_eq!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Published
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concurrent_health_and_close_always_reach_one_terminal_state() {
|
||||
for _ in 0..512 {
|
||||
let session = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
let now = Instant::now();
|
||||
arm_http_health(&session, now);
|
||||
let barrier = Arc::new(Barrier::new(3));
|
||||
let health_session = Arc::clone(&session);
|
||||
let health_barrier = Arc::clone(&barrier);
|
||||
let health = std::thread::spawn(move || {
|
||||
health_barrier.wait();
|
||||
std::thread::yield_now();
|
||||
let claim = {
|
||||
let mut state = health_session.state.lock();
|
||||
health_session.carrier_health_ready_locked(&mut state, now)
|
||||
};
|
||||
if claim.is_some() {
|
||||
health_session.publish_carrier_health();
|
||||
}
|
||||
});
|
||||
let close_session = Arc::clone(&session);
|
||||
let close_barrier = Arc::clone(&barrier);
|
||||
let close = std::thread::spawn(move || {
|
||||
close_barrier.wait();
|
||||
std::thread::yield_now();
|
||||
close_session.close(SessionCloseReason::ApiClose);
|
||||
});
|
||||
barrier.wait();
|
||||
health.join().unwrap();
|
||||
close.join().unwrap();
|
||||
|
||||
assert!(matches!(
|
||||
session.carrier_health_publication_state(),
|
||||
CarrierHealthPublicationState::Published | CarrierHealthPublicationState::Rejected
|
||||
));
|
||||
assert!(!session.publish_carrier_health());
|
||||
assert_eq!(
|
||||
session.close(SessionCloseReason::ApiClose),
|
||||
SessionCloseOutcome::AlreadyClosing
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commit_and_supersede_have_one_session_lock_winner() {
|
||||
let committed = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
{
|
||||
let mut state = committed.state.lock();
|
||||
assert!(
|
||||
committed
|
||||
.record_uplink_progress_locked(
|
||||
&mut state,
|
||||
AppliedProgress {
|
||||
accepted_open: true,
|
||||
accepted_data: true,
|
||||
},
|
||||
)
|
||||
.0
|
||||
);
|
||||
}
|
||||
assert!(!committed.begin_carrier_supersede());
|
||||
|
||||
let replacing = session(WebCarrier::Https, Instant::now() + Duration::from_secs(60));
|
||||
assert!(replacing.begin_carrier_supersede());
|
||||
assert_eq!(
|
||||
replacing.ensure_carrier_active_locked(&replacing.state.lock()),
|
||||
Err(crate::web::manager::ManagerError::Closed)
|
||||
);
|
||||
replacing.cancel_carrier_supersede();
|
||||
assert!(
|
||||
replacing
|
||||
.ensure_carrier_active_locked(&replacing.state.lock())
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user