mirror of
https://github.com/telemt/telemt.git
synced 2026-09-20 09:40:33 +03:00
01ffca5d34
Co-Authored-By: brekotis <93345790+brekotis@users.noreply.github.com>
232 lines
8.7 KiB
Rust
232 lines
8.7 KiB
Rust
use std::net::IpAddr;
|
|
use std::sync::Arc;
|
|
use std::time::Instant;
|
|
|
|
use super::state::CarrierChainPhase;
|
|
use super::{
|
|
CarrierClientClass, CarrierEcho, CarrierLearningContext, CarrierRequest, TokenHash,
|
|
WebProcessRuntime,
|
|
};
|
|
use crate::config::WebCarrier;
|
|
use crate::web::session::WebSession;
|
|
use crate::web::telemetry::WebCarrierLearningOutcome;
|
|
use crate::web::trace::{TraceIdentity, TraceLifecycleEvent};
|
|
|
|
/// Typed result of publishing one exact session health transition.
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum CarrierHealthPublicationOutcome {
|
|
/// Manager ownership was confirmed and the attempt became healthy.
|
|
Published(WebCarrierLearningOutcome),
|
|
/// Manager or transport ownership rejected the publication.
|
|
Rejected(WebCarrierLearningOutcome),
|
|
}
|
|
|
|
impl CarrierHealthPublicationOutcome {
|
|
/// Returns whether manager state accepted the health transition.
|
|
pub(crate) const fn published(self) -> bool {
|
|
matches!(self, Self::Published(_))
|
|
}
|
|
}
|
|
|
|
impl WebProcessRuntime {
|
|
/// Returns authenticated current chain metadata after a committed retry conflict.
|
|
pub(crate) fn carrier_echo(
|
|
&self,
|
|
bootstrap_hash: TokenHash,
|
|
host: &str,
|
|
client_ip: IpAddr,
|
|
request: CarrierRequest,
|
|
) -> Option<CarrierEcho> {
|
|
let state = self.state.lock();
|
|
let entry = state.bootstraps.get(&bootstrap_hash)?;
|
|
let session = entry.session.as_ref()?;
|
|
if !entry.used
|
|
|| entry.profile.host != host
|
|
|| entry.session_client_ip != Some(client_ip)
|
|
|| entry
|
|
.carrier_request
|
|
.is_none_or(|current| !current.matches_client(request))
|
|
|| !(matches!(
|
|
entry.carrier_phase,
|
|
CarrierChainPhase::CommittedPendingHealth | CarrierChainPhase::Healthy
|
|
) || session.is_carrier_committed())
|
|
{
|
|
return None;
|
|
}
|
|
Some(CarrierEcho {
|
|
carrier: session.carrier(),
|
|
attempt: entry.carrier_attempt,
|
|
candidate_count: u8::try_from(entry.carrier_candidates.len()).unwrap_or(4),
|
|
deadline_secs: entry.profile.carrier_negotiation_deadlines_secs[3],
|
|
state: if entry.carrier_phase == CarrierChainPhase::Provisional
|
|
&& session.is_carrier_committed()
|
|
{
|
|
CarrierChainPhase::CommittedPendingHealth.as_str()
|
|
} else {
|
|
entry.carrier_phase.as_str()
|
|
},
|
|
})
|
|
}
|
|
|
|
/// Restores the exact old attempt after successor admission fails.
|
|
pub(super) fn cancel_replacement(
|
|
&self,
|
|
bootstrap_hash: TokenHash,
|
|
old_session: &Arc<WebSession>,
|
|
) {
|
|
old_session.cancel_carrier_supersede();
|
|
let mut state = self.state.lock();
|
|
if let Some(entry) = state.bootstraps.get_mut(&bootstrap_hash)
|
|
&& entry
|
|
.session
|
|
.as_ref()
|
|
.is_some_and(|session| Arc::ptr_eq(session, old_session))
|
|
{
|
|
entry.carrier_transitioning = false;
|
|
}
|
|
}
|
|
|
|
/// Freezes replacement immediately after accepted carrier state mutation.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn carrier_committed(
|
|
self: &Arc<Self>,
|
|
bootstrap_hash: TokenHash,
|
|
session_hash: TokenHash,
|
|
attempt: u8,
|
|
carrier: WebCarrier,
|
|
class: CarrierClientClass,
|
|
client_ip: IpAddr,
|
|
identity: TraceIdentity,
|
|
) -> bool {
|
|
let mut state = self.state.lock();
|
|
let outcome = state.bootstraps.get_mut(&bootstrap_hash).and_then(|entry| {
|
|
if entry.carrier_attempt == attempt
|
|
&& entry
|
|
.session
|
|
.as_ref()
|
|
.is_some_and(|session| session.token_hash() == session_hash)
|
|
&& entry.carrier_phase == CarrierChainPhase::Provisional
|
|
{
|
|
entry.carrier_phase = CarrierChainPhase::CommittedPendingHealth;
|
|
Some((entry.carrier_scores, entry.recovery))
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
drop(state);
|
|
let Some((scores, recovery)) = outcome else {
|
|
return false;
|
|
};
|
|
if recovery {
|
|
self.telemetry.record_bridge_recovery(
|
|
crate::web::telemetry::WebBridgeRecoveryEvent::Committed,
|
|
);
|
|
}
|
|
self.trace.record_carrier_lifecycle(
|
|
client_ip,
|
|
identity.clone(),
|
|
TraceLifecycleEvent::CarrierCommitted,
|
|
class.as_str(),
|
|
carrier,
|
|
attempt,
|
|
scores,
|
|
None,
|
|
);
|
|
true
|
|
}
|
|
|
|
/// Promotes one exact committed attempt after transport-specific health evidence.
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub(crate) fn carrier_became_healthy(
|
|
&self,
|
|
bootstrap_hash: TokenHash,
|
|
session_hash: TokenHash,
|
|
attempt: u8,
|
|
carrier: WebCarrier,
|
|
class: CarrierClientClass,
|
|
learning_context: Option<CarrierLearningContext>,
|
|
client_ip: IpAddr,
|
|
identity: TraceIdentity,
|
|
websocket_owner: Option<u64>,
|
|
) -> CarrierHealthPublicationOutcome {
|
|
if carrier.uses_websocket()
|
|
&& websocket_owner.is_none_or(|owner| {
|
|
!self.claim_websocket_health(owner, session_hash)
|
|
})
|
|
{
|
|
let outcome = WebCarrierLearningOutcome::OwnerNotLive;
|
|
self.telemetry.record_carrier_learning(carrier, outcome);
|
|
return CarrierHealthPublicationOutcome::Rejected(outcome);
|
|
}
|
|
let scores = {
|
|
let mut state = self.state.lock();
|
|
let Some(entry) = state.bootstraps.get_mut(&bootstrap_hash) else {
|
|
let outcome = WebCarrierLearningOutcome::MissingChain;
|
|
self.telemetry.record_carrier_learning(carrier, outcome);
|
|
return CarrierHealthPublicationOutcome::Rejected(outcome);
|
|
};
|
|
if entry.carrier_phase != CarrierChainPhase::CommittedPendingHealth {
|
|
let outcome = WebCarrierLearningOutcome::PhaseMismatch;
|
|
self.telemetry.record_carrier_learning(carrier, outcome);
|
|
return CarrierHealthPublicationOutcome::Rejected(outcome);
|
|
}
|
|
let Some(session) = entry.session.as_ref().filter(|session| {
|
|
entry.carrier_attempt == attempt && session.token_hash() == session_hash
|
|
}) else {
|
|
let outcome = WebCarrierLearningOutcome::SessionMismatch;
|
|
self.telemetry.record_carrier_learning(carrier, outcome);
|
|
return CarrierHealthPublicationOutcome::Rejected(outcome);
|
|
};
|
|
if !session.publish_carrier_health() {
|
|
let outcome = WebCarrierLearningOutcome::ClosedBeforeHealth;
|
|
self.telemetry.record_carrier_learning(carrier, outcome);
|
|
return CarrierHealthPublicationOutcome::Rejected(outcome);
|
|
}
|
|
entry.carrier_phase = CarrierChainPhase::Healthy;
|
|
entry.carrier_scores
|
|
};
|
|
let learning_outcome = if let Some(context) = learning_context {
|
|
let now = Instant::now();
|
|
let outcome = self.learning.lock().record_chain(
|
|
now,
|
|
context.epoch,
|
|
context,
|
|
&[],
|
|
carrier,
|
|
);
|
|
match outcome {
|
|
super::learning::CarrierLearningRecordOutcome::Recorded => {
|
|
WebCarrierLearningOutcome::Recorded
|
|
}
|
|
super::learning::CarrierLearningRecordOutcome::PolicyDisabled => {
|
|
WebCarrierLearningOutcome::PolicyDisabled
|
|
}
|
|
super::learning::CarrierLearningRecordOutcome::StaleEpoch => {
|
|
WebCarrierLearningOutcome::StaleEpoch
|
|
}
|
|
super::learning::CarrierLearningRecordOutcome::CapacityRejected => {
|
|
WebCarrierLearningOutcome::CapacityRejected
|
|
}
|
|
super::learning::CarrierLearningRecordOutcome::SequenceExhausted => {
|
|
WebCarrierLearningOutcome::SequenceExhausted
|
|
}
|
|
}
|
|
} else {
|
|
WebCarrierLearningOutcome::NotEligible
|
|
};
|
|
self.telemetry
|
|
.record_carrier_learning(carrier, learning_outcome);
|
|
self.trace.record_carrier_lifecycle(
|
|
client_ip,
|
|
identity,
|
|
TraceLifecycleEvent::CarrierHealthy,
|
|
class.as_str(),
|
|
carrier,
|
|
attempt,
|
|
scores,
|
|
None,
|
|
);
|
|
CarrierHealthPublicationOutcome::Published(learning_outcome)
|
|
}
|
|
}
|