use super::{ClientParticipantAggregate, DetachReplayStatus, ExpectedOperationState, replay};
use crate::wire::ClientRequest;
#[derive(Debug, PartialEq, Eq)]
pub struct ExpectedParticipantOperation {
request: ClientRequest,
authorization: u64,
}
impl ExpectedParticipantOperation {
#[must_use]
pub const fn request(&self) -> &ClientRequest {
&self.request
}
#[must_use]
pub fn into_request(self) -> (ClientRequest, ClientResponseCorrelation) {
(
self.request,
ClientResponseCorrelation {
authorization: self.authorization,
},
)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ClientResponseCorrelation {
pub(super) authorization: u64,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ClientOperationRecordRefusalReason {
OutstandingOperation,
DetachReplayOutstanding,
DetachReplayIncompatible,
AbandonmentPending,
BindingMismatch,
AlreadyDead,
AmbiguousCorrelation,
AuthorizationExhausted,
}
#[derive(Debug, PartialEq, Eq)]
pub struct ClientOperationRecordRefusal {
aggregate: ClientParticipantAggregate,
request: ClientRequest,
reason: ClientOperationRecordRefusalReason,
}
impl ClientOperationRecordRefusal {
#[must_use]
pub const fn reason(&self) -> ClientOperationRecordRefusalReason {
self.reason
}
#[must_use]
pub fn into_parts(self) -> (ClientParticipantAggregate, ClientRequest) {
(self.aggregate, self.request)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ClientContinuousOperation {
aggregate: ClientParticipantAggregate,
operation: ExpectedParticipantOperation,
}
impl ClientContinuousOperation {
#[must_use]
pub fn into_parts(self) -> (ClientParticipantAggregate, ExpectedParticipantOperation) {
(self.aggregate, self.operation)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ClientPendingOperationRecord {
pub(super) successor: ClientParticipantAggregate,
operation: ExpectedParticipantOperation,
prior_replay: Option<replay::DetachReplayState>,
}
impl ClientPendingOperationRecord {
#[must_use]
pub fn commit(self) -> ClientOperationCommit {
ClientOperationCommit {
aggregate: self.successor,
operation: self.operation,
}
}
#[must_use]
pub fn abort(mut self) -> (ClientParticipantAggregate, ClientRequest) {
self.successor.expected = None;
self.successor.next_operation_authorization =
self.operation.authorization.saturating_sub(1);
if let Some(prior_replay) = self.prior_replay {
self.successor.detach_replay.state = prior_replay;
}
(self.successor, self.operation.request)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ClientOperationCommit {
pub(super) aggregate: ClientParticipantAggregate,
operation: ExpectedParticipantOperation,
}
impl ClientOperationCommit {
#[must_use]
pub fn into_parts(mut self) -> (ClientParticipantAggregate, ExpectedParticipantOperation) {
if let Some(expected) = self.aggregate.expected.as_mut() {
expected.issued = true;
}
if matches!(self.operation.request, ClientRequest::Detach(_)) {
self.aggregate.detach_replay.mark_initial_attempt_started();
}
(self.aggregate, self.operation)
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum RecoveredExpectedOperationDecision {
Recovered {
aggregate: ClientParticipantAggregate,
operation: ExpectedParticipantOperation,
},
NotAvailable {
aggregate: ClientParticipantAggregate,
already_issued: bool,
},
}
#[must_use]
pub fn recover_expected_operation(
mut aggregate: ClientParticipantAggregate,
) -> RecoveredExpectedOperationDecision {
let Some(expected) = aggregate.expected.as_mut() else {
return RecoveredExpectedOperationDecision::NotAvailable {
aggregate,
already_issued: false,
};
};
if expected.issued {
return RecoveredExpectedOperationDecision::NotAvailable {
aggregate,
already_issued: true,
};
}
expected.issued = true;
let request = expected.request.clone();
let authorization = expected.authorization;
if matches!(request, ClientRequest::Detach(_)) {
aggregate.detach_replay.mark_initial_attempt_started();
}
RecoveredExpectedOperationDecision::Recovered {
aggregate,
operation: ExpectedParticipantOperation {
request,
authorization,
},
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LostAuthorityResolutionRefusalReason {
NoPendingTestimony,
}
#[derive(Debug, PartialEq, Eq)]
pub enum LostOperationAuthorityDecision {
Recorded {
aggregate: ClientParticipantAggregate,
request: ClientRequest,
testimony: super::LostAuthorityTestimony,
},
DetachParked {
aggregate: ClientParticipantAggregate,
request: ClientRequest,
testimony: super::LostAuthorityTestimony,
},
Refused {
aggregate: ClientParticipantAggregate,
reason: LostAuthorityResolutionRefusalReason,
},
}
#[must_use]
pub fn resolve_lost_operation_authority(
mut aggregate: ClientParticipantAggregate,
) -> LostOperationAuthorityDecision {
let Some(expected) = aggregate.expected.as_mut() else {
return LostOperationAuthorityDecision::Refused {
aggregate,
reason: LostAuthorityResolutionRefusalReason::NoPendingTestimony,
};
};
let Some(testimony) = expected.lost.take() else {
return LostOperationAuthorityDecision::Refused {
aggregate,
reason: LostAuthorityResolutionRefusalReason::NoPendingTestimony,
};
};
if matches!(expected.request, ClientRequest::Detach(_)) {
expected.issued = false;
let request = expected.request.clone();
if let replay::DetachReplayState::Recorded { status, .. } =
&mut aggregate.detach_replay.state
{
*status = DetachReplayStatus::Parked;
}
return LostOperationAuthorityDecision::DetachParked {
aggregate,
request,
testimony,
};
}
let request = expected.request.clone();
aggregate.expected = None;
LostOperationAuthorityDecision::Recorded {
aggregate,
request,
testimony,
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExpectedOperationTransportFate {
ResponseUnavailable,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExpectedOperationFateRefusalReason {
NoIssuedOperation,
StaleCorrelation,
DetachUsesReplayFate,
LostAuthorityPending,
}
#[derive(Debug, PartialEq, Eq)]
pub enum ExpectedOperationFateDecision {
Recorded {
aggregate: ClientParticipantAggregate,
request: ClientRequest,
fate: ExpectedOperationTransportFate,
},
Refused {
aggregate: ClientParticipantAggregate,
correlation: ClientResponseCorrelation,
fate: ExpectedOperationTransportFate,
reason: ExpectedOperationFateRefusalReason,
},
}
#[must_use]
pub fn record_expected_operation_fate(
mut aggregate: ClientParticipantAggregate,
correlation: ClientResponseCorrelation,
fate: ExpectedOperationTransportFate,
) -> ExpectedOperationFateDecision {
let Some(expected) = aggregate.expected.as_ref() else {
return ExpectedOperationFateDecision::Refused {
aggregate,
correlation,
fate,
reason: ExpectedOperationFateRefusalReason::NoIssuedOperation,
};
};
if !expected.issued {
return ExpectedOperationFateDecision::Refused {
aggregate,
correlation,
fate,
reason: ExpectedOperationFateRefusalReason::NoIssuedOperation,
};
}
if expected.lost.is_some() {
return ExpectedOperationFateDecision::Refused {
aggregate,
correlation,
fate,
reason: ExpectedOperationFateRefusalReason::LostAuthorityPending,
};
}
if expected.authorization != correlation.authorization {
return ExpectedOperationFateDecision::Refused {
aggregate,
correlation,
fate,
reason: ExpectedOperationFateRefusalReason::StaleCorrelation,
};
}
if matches!(expected.request, ClientRequest::Detach(_)) {
return ExpectedOperationFateDecision::Refused {
aggregate,
correlation,
fate,
reason: ExpectedOperationFateRefusalReason::DetachUsesReplayFate,
};
}
let expected = aggregate.expected.take();
match expected {
Some(expected) => ExpectedOperationFateDecision::Recorded {
aggregate,
request: expected.request,
fate,
},
None => ExpectedOperationFateDecision::Refused {
aggregate,
correlation,
fate,
reason: ExpectedOperationFateRefusalReason::NoIssuedOperation,
},
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum ClientOperationRecordDecision {
Pending(ClientPendingOperationRecord),
Continuous(ClientContinuousOperation),
Refused(ClientOperationRecordRefusal),
}
#[must_use]
pub fn record_operation(
mut aggregate: ClientParticipantAggregate,
request: ClientRequest,
) -> ClientOperationRecordDecision {
if !aggregate.binding.accepts_request(&request) {
let reason = if aggregate.binding.is_left() {
ClientOperationRecordRefusalReason::AlreadyDead
} else {
ClientOperationRecordRefusalReason::BindingMismatch
};
return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
aggregate,
request,
reason,
});
}
if matches!(request, ClientRequest::ParticipantAck(_)) {
return ClientOperationRecordDecision::Continuous(ClientContinuousOperation {
aggregate,
operation: ExpectedParticipantOperation {
request,
authorization: 0,
},
});
}
if aggregate.expected.is_some() {
return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
aggregate,
request,
reason: ClientOperationRecordRefusalReason::OutstandingOperation,
});
}
if aggregate.restored_abandonment.is_some()
&& matches!(request, ClientRequest::ObserverRecovery(_))
{
return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
aggregate,
request,
reason: ClientOperationRecordRefusalReason::AbandonmentPending,
});
}
let Some(authorization) = aggregate.next_operation_authorization.checked_add(1) else {
return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
aggregate,
request,
reason: ClientOperationRecordRefusalReason::AuthorizationExhausted,
});
};
let prior_replay = match admit_detach_replay(&mut aggregate, &request) {
Ok(prior_replay) => prior_replay,
Err(reason) => {
return ClientOperationRecordDecision::Refused(ClientOperationRecordRefusal {
aggregate,
request,
reason,
});
}
};
aggregate.next_operation_authorization = authorization;
aggregate.expected = Some(ExpectedOperationState {
request: request.clone(),
issued: false,
authorization,
lost: None,
});
ClientOperationRecordDecision::Pending(ClientPendingOperationRecord {
successor: aggregate,
operation: ExpectedParticipantOperation {
request,
authorization,
},
prior_replay,
})
}
fn admit_detach_replay(
aggregate: &mut ClientParticipantAggregate,
request: &ClientRequest,
) -> Result<Option<replay::DetachReplayState>, ClientOperationRecordRefusalReason> {
let ClientRequest::Detach(value) = request else {
return Ok(None);
};
let envelope = crate::wire::DetachEnvelope {
conversation_id: value.conversation_id,
participant_id: value.participant_id,
capability_generation: value.capability_generation,
detach_attempt_token: value.detach_attempt_token,
};
match &aggregate.detach_replay.state {
replay::DetachReplayState::Empty => {
let prior = aggregate.detach_replay.state.clone();
aggregate.detach_replay.state = replay::DetachReplayState::Recorded {
request: envelope,
status: DetachReplayStatus::Parked,
};
Ok(Some(prior))
}
replay::DetachReplayState::Recorded {
request: retained,
status,
} if retained == &envelope => {
if matches!(status, DetachReplayStatus::Parked) {
Ok(Some(aggregate.detach_replay.state.clone()))
} else {
Err(ClientOperationRecordRefusalReason::DetachReplayIncompatible)
}
}
replay::DetachReplayState::Recorded { .. }
if aggregate.detach_replay.can_replace_with(&envelope) =>
{
let prior = aggregate.detach_replay.state.clone();
aggregate.detach_replay.state = replay::DetachReplayState::Recorded {
request: envelope,
status: DetachReplayStatus::Parked,
};
Ok(Some(prior))
}
replay::DetachReplayState::Recorded { .. } => {
Err(ClientOperationRecordRefusalReason::DetachReplayOutstanding)
}
}
}