Unify client Domain observation
This commit is contained in:
parent
30cd36dcdd
commit
786049534d
3 changed files with 443 additions and 120 deletions
|
|
@ -6,7 +6,6 @@ use std::io::{self, IsTerminal, Write};
|
||||||
use std::os::fd::{AsFd, AsRawFd};
|
use std::os::fd::{AsFd, AsRawFd};
|
||||||
use std::os::unix::fs::MetadataExt;
|
use std::os::unix::fs::MetadataExt;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::thread;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use lazaret_core::configuration::{
|
use lazaret_core::configuration::{
|
||||||
|
|
@ -25,6 +24,10 @@ use lazaret_runtime_supervisor::sandbox::default_sandbox_layout;
|
||||||
use rustix::fs::{Mode, OFlags, openat};
|
use rustix::fs::{Mode, OFlags, openat};
|
||||||
use rustix::pipe::{PipeFlags, pipe_with};
|
use rustix::pipe::{PipeFlags, pipe_with};
|
||||||
|
|
||||||
|
use crate::domain_observation::{
|
||||||
|
DiscoveryAction, DiscoveryState, DomainObservation, ObservationPath, SystemObservationClock,
|
||||||
|
WaiterAction,
|
||||||
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
BootstrapAcquisition, ClientBackend, ClientCommand, ClientError, ClientSessionIdentity,
|
BootstrapAcquisition, ClientBackend, ClientCommand, ClientError, ClientSessionIdentity,
|
||||||
ControlEndpoint, DomainStatus, execute_interactive_over_control_face_as,
|
ControlEndpoint, DomainStatus, execute_interactive_over_control_face_as,
|
||||||
|
|
@ -176,22 +179,60 @@ impl CoordinatorClientBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn await_publication(
|
fn observe_domain(
|
||||||
&mut self,
|
&mut self,
|
||||||
|
initial_path: ObservationPath,
|
||||||
) -> Result<ControlEndpoint, ClientError<CoordinatorClientError>> {
|
) -> Result<ControlEndpoint, ClientError<CoordinatorClientError>> {
|
||||||
|
let waiter = if initial_path == ObservationPath::WaitingClient {
|
||||||
|
let configuration = self.configuration().map_err(ClientError::Backend)?;
|
||||||
|
let runtime_authority = configuration
|
||||||
|
.runtime_domain_root()
|
||||||
|
.open_authority()
|
||||||
|
.map_err(failure)
|
||||||
|
.map_err(ClientError::Backend)?;
|
||||||
|
let socket = PathBuf::from(format!(
|
||||||
|
"/proc/self/fd/{}/{}",
|
||||||
|
runtime_authority.as_raw_fd(),
|
||||||
|
BOOTSTRAP_WAITER_SOCKET_NAME
|
||||||
|
));
|
||||||
|
let request = BootstrapWaitRequest {
|
||||||
|
protocol_version: lazaret_core::records::ProtocolVersion::V1,
|
||||||
|
domain_id: configuration.domain_id(),
|
||||||
|
configuration_fingerprint: configuration.configuration_fingerprint(),
|
||||||
|
cli_id: self.identity.cli_id(),
|
||||||
|
session_id: self.identity.session_id(),
|
||||||
|
session_anchor_id: self.identity.session_anchor_id(),
|
||||||
|
};
|
||||||
|
Some((runtime_authority, socket, request))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let mut observer = DomainObservation::new(initial_path, SystemObservationClock::start());
|
||||||
|
let mut waiter_transfer = None;
|
||||||
loop {
|
loop {
|
||||||
match self.discover().map_err(ClientError::Backend)? {
|
let (state, endpoint) = match self.discover().map_err(ClientError::Backend)? {
|
||||||
Discovery::Ready(endpoint) => return Ok(*endpoint),
|
Discovery::Ready(endpoint) => (DiscoveryState::Ready, Some(*endpoint)),
|
||||||
Discovery::Stale | Discovery::Absent => {}
|
Discovery::Stale => (DiscoveryState::Stale, None),
|
||||||
|
Discovery::Absent => (DiscoveryState::Absent, None),
|
||||||
|
};
|
||||||
|
match observer.discovery(state) {
|
||||||
|
DiscoveryAction::ReturnReady => {
|
||||||
|
let endpoint = endpoint.expect("ready discovery supplies an endpoint");
|
||||||
|
return Ok(match waiter_transfer {
|
||||||
|
Some(stream) => endpoint.retain_waiter_transfer(stream),
|
||||||
|
None => endpoint,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if self
|
DiscoveryAction::RefuseStale => return Err(ClientError::StaleCoordinator),
|
||||||
|
DiscoveryAction::CheckCoordinator => {
|
||||||
|
let exited = self
|
||||||
.coordinator
|
.coordinator
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or_else(|| ClientError::Backend(failure("Host Coordinator is absent")))?
|
.ok_or_else(|| ClientError::Backend(failure("Host Coordinator is absent")))?
|
||||||
.has_exited()
|
.has_exited()
|
||||||
.map_err(failure)
|
.map_err(failure)
|
||||||
.map_err(ClientError::Backend)?
|
.map_err(ClientError::Backend)?;
|
||||||
{
|
if observer.coordinator_exited(exited) {
|
||||||
let status = self
|
let status = self
|
||||||
.coordinator
|
.coordinator
|
||||||
.take()
|
.take()
|
||||||
|
|
@ -203,7 +244,95 @@ impl CoordinatorClientBackend {
|
||||||
"Host Coordinator exited before publication with {status:?}"
|
"Host Coordinator exited before publication with {status:?}"
|
||||||
))));
|
))));
|
||||||
}
|
}
|
||||||
thread::sleep(Duration::from_millis(10));
|
}
|
||||||
|
DiscoveryAction::AttemptWaiter => {
|
||||||
|
let (_, socket, request) = waiter
|
||||||
|
.as_ref()
|
||||||
|
.expect("waiting observation retains waiter authority");
|
||||||
|
if let Some(stream) = self.attempt_waiter(&observer, socket, request)? {
|
||||||
|
waiter_transfer = Some(stream);
|
||||||
|
observer.transfer_publication();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DiscoveryAction::Retry => {}
|
||||||
|
}
|
||||||
|
observer
|
||||||
|
.retry()
|
||||||
|
.map_err(|_| observation_deadline(observer.path()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attempt_waiter(
|
||||||
|
&self,
|
||||||
|
observer: &DomainObservation<SystemObservationClock>,
|
||||||
|
socket: &std::path::Path,
|
||||||
|
request: &BootstrapWaitRequest,
|
||||||
|
) -> Result<Option<std::os::unix::net::UnixStream>, ClientError<CoordinatorClientError>> {
|
||||||
|
let mut stream = match std::os::unix::net::UnixStream::connect(socket) {
|
||||||
|
Ok(stream) => stream,
|
||||||
|
Err(error)
|
||||||
|
if matches!(
|
||||||
|
error.kind(),
|
||||||
|
io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
|
||||||
|
) =>
|
||||||
|
{
|
||||||
|
if observer.lock_available(
|
||||||
|
self.domain_lock_is_available()
|
||||||
|
.map_err(ClientError::Backend)?,
|
||||||
|
) == Some(WaiterAction::RefuseFailed)
|
||||||
|
{
|
||||||
|
return Err(ClientError::WaitingDomainFailed);
|
||||||
|
}
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Err(error) => return Err(ClientError::Backend(failure(error))),
|
||||||
|
};
|
||||||
|
stream
|
||||||
|
.set_read_timeout(Some(observation_remaining(observer)?))
|
||||||
|
.map_err(failure)
|
||||||
|
.map_err(ClientError::Backend)?;
|
||||||
|
write_frame(&mut stream, request).map_err(|error| {
|
||||||
|
ClientError::Backend(failure(format!("bootstrap waiter request failed: {error}")))
|
||||||
|
})?;
|
||||||
|
let first: BootstrapWaitOutcome = read_frame(&mut stream).map_err(|error| {
|
||||||
|
ClientError::Backend(failure(format!(
|
||||||
|
"bootstrap waiter admission failed: {error}"
|
||||||
|
)))
|
||||||
|
})?;
|
||||||
|
let mut action = observer.waiter(first, false);
|
||||||
|
if action == WaiterAction::AwaitTerminal {
|
||||||
|
action = match read_frame(&mut stream) {
|
||||||
|
Ok(outcome) => observer.waiter(outcome, true),
|
||||||
|
Err(_) => WaiterAction::RefuseFailed,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match action {
|
||||||
|
WaiterAction::AwaitPublication => Ok(Some(stream)),
|
||||||
|
WaiterAction::RefuseStale => Err(ClientError::StaleCoordinator),
|
||||||
|
WaiterAction::RefuseFailed => Err(ClientError::WaitingDomainFailed),
|
||||||
|
WaiterAction::RefuseProtocol | WaiterAction::AwaitTerminal => Err(
|
||||||
|
ClientError::Protocol("bootstrap waiter sent an invalid terminal outcome"),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observation_remaining(
|
||||||
|
observer: &DomainObservation<SystemObservationClock>,
|
||||||
|
) -> Result<Duration, ClientError<CoordinatorClientError>> {
|
||||||
|
observer
|
||||||
|
.remaining()
|
||||||
|
.map_err(|_| observation_deadline(observer.path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn observation_deadline(path: ObservationPath) -> ClientError<CoordinatorClientError> {
|
||||||
|
match path {
|
||||||
|
ObservationPath::BootstrapLeader => {
|
||||||
|
ClientError::Backend(failure("Host Coordinator publication deadline expired"))
|
||||||
|
}
|
||||||
|
ObservationPath::WaitingClient | ObservationPath::PublicationTransfer => {
|
||||||
|
ClientError::WaitingDomainFailed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -247,85 +376,11 @@ impl ClientBackend for CoordinatorClientBackend {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn bootstrap(&mut self) -> Result<Self::Endpoint, ClientError<Self::Error>> {
|
fn bootstrap(&mut self) -> Result<Self::Endpoint, ClientError<Self::Error>> {
|
||||||
self.await_publication()
|
self.observe_domain(ObservationPath::BootstrapLeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wait_until_ready(&mut self) -> Result<Self::Endpoint, ClientError<Self::Error>> {
|
fn wait_until_ready(&mut self) -> Result<Self::Endpoint, ClientError<Self::Error>> {
|
||||||
let configuration = self.configuration().map_err(ClientError::Backend)?;
|
self.observe_domain(ObservationPath::WaitingClient)
|
||||||
let runtime_authority = configuration
|
|
||||||
.runtime_domain_root()
|
|
||||||
.open_authority()
|
|
||||||
.map_err(failure)
|
|
||||||
.map_err(ClientError::Backend)?;
|
|
||||||
let socket = PathBuf::from(format!(
|
|
||||||
"/proc/self/fd/{}/{}",
|
|
||||||
runtime_authority.as_raw_fd(),
|
|
||||||
BOOTSTRAP_WAITER_SOCKET_NAME
|
|
||||||
));
|
|
||||||
let request = BootstrapWaitRequest {
|
|
||||||
protocol_version: lazaret_core::records::ProtocolVersion::V1,
|
|
||||||
domain_id: configuration.domain_id(),
|
|
||||||
configuration_fingerprint: configuration.configuration_fingerprint(),
|
|
||||||
cli_id: self.identity.cli_id(),
|
|
||||||
session_id: self.identity.session_id(),
|
|
||||||
session_anchor_id: self.identity.session_anchor_id(),
|
|
||||||
};
|
|
||||||
loop {
|
|
||||||
match self.discover().map_err(ClientError::Backend)? {
|
|
||||||
Discovery::Ready(endpoint) => return Ok(*endpoint),
|
|
||||||
Discovery::Stale | Discovery::Absent => {}
|
|
||||||
}
|
|
||||||
match std::os::unix::net::UnixStream::connect(&socket) {
|
|
||||||
Ok(mut stream) => {
|
|
||||||
write_frame(&mut stream, &request).map_err(|error| {
|
|
||||||
ClientError::Backend(failure(format!(
|
|
||||||
"bootstrap waiter request failed: {error}"
|
|
||||||
)))
|
|
||||||
})?;
|
|
||||||
let first: BootstrapWaitOutcome = read_frame(&mut stream).map_err(|error| {
|
|
||||||
ClientError::Backend(failure(format!(
|
|
||||||
"bootstrap waiter admission failed: {error}"
|
|
||||||
)))
|
|
||||||
})?;
|
|
||||||
if first != BootstrapWaitOutcome::Waiting {
|
|
||||||
return match first {
|
|
||||||
BootstrapWaitOutcome::Refused => Err(ClientError::StaleCoordinator),
|
|
||||||
BootstrapWaitOutcome::Failed => Err(ClientError::WaitingDomainFailed),
|
|
||||||
BootstrapWaitOutcome::Published => {
|
|
||||||
self.await_ready_publication(Some(stream))
|
|
||||||
}
|
|
||||||
BootstrapWaitOutcome::Waiting => unreachable!(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return match read_frame(&mut stream) {
|
|
||||||
Ok(BootstrapWaitOutcome::Published) => {
|
|
||||||
self.await_ready_publication(Some(stream))
|
|
||||||
}
|
|
||||||
Ok(BootstrapWaitOutcome::Failed) | Err(_) => {
|
|
||||||
Err(ClientError::WaitingDomainFailed)
|
|
||||||
}
|
|
||||||
Ok(BootstrapWaitOutcome::Waiting | BootstrapWaitOutcome::Refused) => {
|
|
||||||
Err(ClientError::Protocol(
|
|
||||||
"bootstrap waiter sent an invalid terminal outcome",
|
|
||||||
))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
Err(error)
|
|
||||||
if matches!(
|
|
||||||
error.kind(),
|
|
||||||
io::ErrorKind::NotFound | io::ErrorKind::ConnectionRefused
|
|
||||||
) => {}
|
|
||||||
Err(error) => return Err(ClientError::Backend(failure(error))),
|
|
||||||
}
|
|
||||||
if self
|
|
||||||
.domain_lock_is_available()
|
|
||||||
.map_err(ClientError::Backend)?
|
|
||||||
{
|
|
||||||
return Err(ClientError::WaitingDomainFailed);
|
|
||||||
}
|
|
||||||
thread::sleep(Duration::from_millis(10));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(
|
fn execute(
|
||||||
|
|
@ -362,27 +417,6 @@ impl ClientBackend for CoordinatorClientBackend {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CoordinatorClientBackend {
|
|
||||||
fn await_ready_publication(
|
|
||||||
&self,
|
|
||||||
waiter: Option<std::os::unix::net::UnixStream>,
|
|
||||||
) -> Result<ControlEndpoint, ClientError<CoordinatorClientError>> {
|
|
||||||
loop {
|
|
||||||
match self.discover().map_err(ClientError::Backend)? {
|
|
||||||
Discovery::Ready(endpoint) => {
|
|
||||||
let endpoint = *endpoint;
|
|
||||||
return Ok(match waiter {
|
|
||||||
Some(stream) => endpoint.retain_waiter_transfer(stream),
|
|
||||||
None => endpoint,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Discovery::Stale => return Err(ClientError::StaleCoordinator),
|
|
||||||
Discovery::Absent => thread::sleep(Duration::from_millis(1)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn open_private_domain_lock(root: &impl AsFd) -> Result<File, CoordinatorClientError> {
|
pub(crate) fn open_private_domain_lock(root: &impl AsFd) -> Result<File, CoordinatorClientError> {
|
||||||
let file = File::from(
|
let file = File::from(
|
||||||
openat(
|
openat(
|
||||||
|
|
|
||||||
288
crates/lazaret-client/src/domain_observation.rs
Normal file
288
crates/lazaret-client/src/domain_observation.rs
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum ObservationPath {
|
||||||
|
BootstrapLeader,
|
||||||
|
WaitingClient,
|
||||||
|
PublicationTransfer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum DiscoveryState {
|
||||||
|
Ready,
|
||||||
|
Stale,
|
||||||
|
Absent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum DiscoveryAction {
|
||||||
|
ReturnReady,
|
||||||
|
CheckCoordinator,
|
||||||
|
AttemptWaiter,
|
||||||
|
Retry,
|
||||||
|
RefuseStale,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum WaiterAction {
|
||||||
|
AwaitTerminal,
|
||||||
|
AwaitPublication,
|
||||||
|
RefuseStale,
|
||||||
|
RefuseFailed,
|
||||||
|
RefuseProtocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
pub(crate) enum ObservationFailure {
|
||||||
|
DeadlineExceeded,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct ObservationPolicy {
|
||||||
|
retry: Duration,
|
||||||
|
deadline: Duration,
|
||||||
|
}
|
||||||
|
|
||||||
|
const BOOTSTRAP_POLICY: ObservationPolicy = ObservationPolicy {
|
||||||
|
retry: Duration::from_millis(10),
|
||||||
|
deadline: Duration::from_secs(30),
|
||||||
|
};
|
||||||
|
const WAITING_POLICY: ObservationPolicy = ObservationPolicy {
|
||||||
|
retry: Duration::from_millis(10),
|
||||||
|
deadline: Duration::from_secs(30),
|
||||||
|
};
|
||||||
|
const TRANSFER_POLICY: ObservationPolicy = ObservationPolicy {
|
||||||
|
retry: Duration::from_millis(1),
|
||||||
|
deadline: Duration::from_secs(30),
|
||||||
|
};
|
||||||
|
|
||||||
|
pub(crate) trait ObservationClock {
|
||||||
|
fn elapsed(&self) -> Duration;
|
||||||
|
fn wait(&mut self, duration: Duration);
|
||||||
|
fn restart(&mut self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct SystemObservationClock {
|
||||||
|
started: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemObservationClock {
|
||||||
|
pub(crate) fn start() -> Self {
|
||||||
|
Self {
|
||||||
|
started: Instant::now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObservationClock for SystemObservationClock {
|
||||||
|
fn elapsed(&self) -> Duration {
|
||||||
|
self.started.elapsed()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait(&mut self, duration: Duration) {
|
||||||
|
std::thread::sleep(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restart(&mut self) {
|
||||||
|
self.started = Instant::now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct DomainObservation<C> {
|
||||||
|
path: ObservationPath,
|
||||||
|
clock: C,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<C: ObservationClock> DomainObservation<C> {
|
||||||
|
pub(crate) fn new(path: ObservationPath, clock: C) -> Self {
|
||||||
|
Self { path, clock }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn path(&self) -> ObservationPath {
|
||||||
|
self.path
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn transfer_publication(&mut self) {
|
||||||
|
self.path = ObservationPath::PublicationTransfer;
|
||||||
|
self.clock.restart();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn discovery(&self, state: DiscoveryState) -> DiscoveryAction {
|
||||||
|
match (self.path, state) {
|
||||||
|
(_, DiscoveryState::Ready) => DiscoveryAction::ReturnReady,
|
||||||
|
(ObservationPath::BootstrapLeader, _) => DiscoveryAction::CheckCoordinator,
|
||||||
|
(ObservationPath::WaitingClient, _) => DiscoveryAction::AttemptWaiter,
|
||||||
|
(ObservationPath::PublicationTransfer, DiscoveryState::Stale) => {
|
||||||
|
DiscoveryAction::RefuseStale
|
||||||
|
}
|
||||||
|
(ObservationPath::PublicationTransfer, DiscoveryState::Absent) => {
|
||||||
|
DiscoveryAction::Retry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn waiter(
|
||||||
|
&self,
|
||||||
|
outcome: lazaret_host_coordinator::waiter::BootstrapWaitOutcome,
|
||||||
|
admitted: bool,
|
||||||
|
) -> WaiterAction {
|
||||||
|
use lazaret_host_coordinator::waiter::BootstrapWaitOutcome::{
|
||||||
|
Failed, Published, Refused, Waiting,
|
||||||
|
};
|
||||||
|
if self.path != ObservationPath::WaitingClient {
|
||||||
|
return WaiterAction::RefuseProtocol;
|
||||||
|
}
|
||||||
|
match (admitted, outcome) {
|
||||||
|
(false, Waiting) => WaiterAction::AwaitTerminal,
|
||||||
|
(_, Published) => WaiterAction::AwaitPublication,
|
||||||
|
(false, Refused) => WaiterAction::RefuseStale,
|
||||||
|
(_, Failed) => WaiterAction::RefuseFailed,
|
||||||
|
_ => WaiterAction::RefuseProtocol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lock_available(&self, available: bool) -> Option<WaiterAction> {
|
||||||
|
(self.path == ObservationPath::WaitingClient && available)
|
||||||
|
.then_some(WaiterAction::RefuseFailed)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn coordinator_exited(&self, exited: bool) -> bool {
|
||||||
|
self.path == ObservationPath::BootstrapLeader && exited
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn remaining(&self) -> Result<Duration, ObservationFailure> {
|
||||||
|
let remaining = self.policy().deadline.saturating_sub(self.clock.elapsed());
|
||||||
|
if remaining.is_zero() {
|
||||||
|
Err(ObservationFailure::DeadlineExceeded)
|
||||||
|
} else {
|
||||||
|
Ok(remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn retry(&mut self) -> Result<(), ObservationFailure> {
|
||||||
|
let remaining = self.remaining()?;
|
||||||
|
self.clock.wait(self.policy().retry.min(remaining));
|
||||||
|
self.remaining().map(|_| ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn policy(&self) -> ObservationPolicy {
|
||||||
|
match self.path {
|
||||||
|
ObservationPath::BootstrapLeader => BOOTSTRAP_POLICY,
|
||||||
|
ObservationPath::WaitingClient => WAITING_POLICY,
|
||||||
|
ObservationPath::PublicationTransfer => TRANSFER_POLICY,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use lazaret_host_coordinator::waiter::BootstrapWaitOutcome;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FakeClock {
|
||||||
|
elapsed: Duration,
|
||||||
|
waits: Vec<Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObservationClock for FakeClock {
|
||||||
|
fn elapsed(&self) -> Duration {
|
||||||
|
self.elapsed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wait(&mut self, duration: Duration) {
|
||||||
|
self.elapsed += duration;
|
||||||
|
self.waits.push(duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restart(&mut self) {
|
||||||
|
self.elapsed = Duration::ZERO;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_path_has_explicit_discovery_actions() {
|
||||||
|
let cases = [
|
||||||
|
(
|
||||||
|
ObservationPath::BootstrapLeader,
|
||||||
|
[
|
||||||
|
DiscoveryAction::ReturnReady,
|
||||||
|
DiscoveryAction::CheckCoordinator,
|
||||||
|
DiscoveryAction::CheckCoordinator,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ObservationPath::WaitingClient,
|
||||||
|
[
|
||||||
|
DiscoveryAction::ReturnReady,
|
||||||
|
DiscoveryAction::AttemptWaiter,
|
||||||
|
DiscoveryAction::AttemptWaiter,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
(
|
||||||
|
ObservationPath::PublicationTransfer,
|
||||||
|
[
|
||||||
|
DiscoveryAction::ReturnReady,
|
||||||
|
DiscoveryAction::RefuseStale,
|
||||||
|
DiscoveryAction::Retry,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (path, expected) in cases {
|
||||||
|
let observer = DomainObservation::new(path, FakeClock::default());
|
||||||
|
assert_eq!(observer.discovery(DiscoveryState::Ready), expected[0]);
|
||||||
|
assert_eq!(observer.discovery(DiscoveryState::Stale), expected[1]);
|
||||||
|
assert_eq!(observer.discovery(DiscoveryState::Absent), expected[2]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn waiter_lock_and_coordinator_outcomes_are_closed() {
|
||||||
|
let observer = DomainObservation::new(ObservationPath::WaitingClient, FakeClock::default());
|
||||||
|
assert_eq!(
|
||||||
|
observer.waiter(BootstrapWaitOutcome::Waiting, false),
|
||||||
|
WaiterAction::AwaitTerminal
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
observer.waiter(BootstrapWaitOutcome::Published, false),
|
||||||
|
WaiterAction::AwaitPublication
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
observer.waiter(BootstrapWaitOutcome::Failed, true),
|
||||||
|
WaiterAction::RefuseFailed
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
observer.waiter(BootstrapWaitOutcome::Refused, false),
|
||||||
|
WaiterAction::RefuseStale
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
observer.waiter(BootstrapWaitOutcome::Waiting, true),
|
||||||
|
WaiterAction::RefuseProtocol
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
observer.lock_available(true),
|
||||||
|
Some(WaiterAction::RefuseFailed)
|
||||||
|
);
|
||||||
|
assert_eq!(observer.lock_available(false), None);
|
||||||
|
|
||||||
|
let leader = DomainObservation::new(ObservationPath::BootstrapLeader, FakeClock::default());
|
||||||
|
assert!(leader.coordinator_exited(true));
|
||||||
|
assert!(!leader.coordinator_exited(false));
|
||||||
|
assert!(!observer.coordinator_exited(true));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retry_deadlines_and_transfer_restart_are_deterministic() {
|
||||||
|
let mut observer =
|
||||||
|
DomainObservation::new(ObservationPath::WaitingClient, FakeClock::default());
|
||||||
|
observer.retry().unwrap();
|
||||||
|
assert_eq!(observer.clock.waits, [Duration::from_millis(10)]);
|
||||||
|
observer.clock.elapsed = WAITING_POLICY.deadline;
|
||||||
|
assert_eq!(observer.retry(), Err(ObservationFailure::DeadlineExceeded));
|
||||||
|
observer.transfer_publication();
|
||||||
|
assert_eq!(observer.path(), ObservationPath::PublicationTransfer);
|
||||||
|
assert_eq!(observer.clock.elapsed, Duration::ZERO);
|
||||||
|
observer.retry().unwrap();
|
||||||
|
assert_eq!(observer.clock.waits.last(), Some(&Duration::from_millis(1)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,6 +22,7 @@ use crate::terminal::{HostTerminal, InteractiveSessionTransport, run_interactive
|
||||||
|
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod coordinator_backend;
|
pub mod coordinator_backend;
|
||||||
|
mod domain_observation;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue