1use std::any::Any;
55use std::collections::BTreeMap;
56use std::fmt;
57use std::io;
58use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
59use std::sync::{Arc, Mutex, MutexGuard};
60use std::time::{Duration, Instant};
61
62use rightkit_child_process::{
63 AdoptedIdentity, Generation, IdentityCheck, OwnedChild, RestartOutcome, RestartPolicy,
64 RestartTracker, WaitOutcome,
65};
66use serde_json::Value;
67
68use crate::client::{Client, ClientConfig};
69use crate::discovery::{
70 record_process_matches, try_adopt_or_spawn, AdoptOrSpawn, SpawnClaim, SpawnClaimError,
71};
72use crate::error::{ConnectError, ServiceError, ERROR_UNAVAILABLE};
73use crate::host_identity::{HostExpectation, HostIdentityError, HostIdentityReason};
74use crate::record::{try_read_record, ServiceRecord};
75
76pub use crate::incumbent::{stop_incumbent, IncumbentError, IncumbentStop};
77pub use crate::shutdown::{OrderedShutdown, PhaseOutcome, PhaseReport};
78use std::num::NonZeroU32;
79
80pub const ERROR_STALE_GENERATION: &str = "stale_generation";
83pub const ERROR_NOT_OWNED: &str = "not_owned";
85pub const ERROR_RESTART_EXHAUSTED: &str = "restart_exhausted";
87pub const ERROR_SESSION_RELEASED: &str = "session_released";
89pub const ERROR_LAUNCH_FAILED: &str = "launch_failed";
91
92fn lock<T>(m: &Mutex<T>) -> MutexGuard<'_, T> {
93 m.lock().unwrap_or_else(|e| e.into_inner())
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
98pub enum Provenance {
99 Owned,
101 Attached,
103}
104
105impl Provenance {
106 pub fn as_str(self) -> &'static str {
107 match self {
108 Provenance::Owned => "owned",
109 Provenance::Attached => "attached",
110 }
111 }
112
113 pub fn may_stop_host(self) -> bool {
115 matches!(self, Provenance::Owned)
116 }
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Hash)]
127pub struct HostGeneration {
128 pub service: String,
129 pub endpoint: String,
130 pub pid: u32,
131 pub process_started_ms: u64,
133 pub started_ms: u64,
135}
136
137impl HostGeneration {
138 pub fn of(record: &ServiceRecord) -> Self {
140 Self {
141 service: record.service.clone(),
142 endpoint: record.endpoint.clone(),
143 pid: record.pid,
144 process_started_ms: record.process_started_ms,
145 started_ms: record.started_ms,
146 }
147 }
148}
149
150#[derive(Debug)]
152#[non_exhaustive]
153pub enum LifecycleError {
154 Service(ServiceError),
156 HostIdentity(HostIdentityError),
159 NoHost,
161 Launch(io::Error),
163 LaunchTimeout,
165 RestartExhausted,
167 NotOwned,
169 Released,
171 StaleGeneration { issued: u64, current: u64 },
175 Unconfirmed,
178 Process(io::Error),
180}
181
182impl LifecycleError {
183 pub fn code(&self) -> &str {
185 match self {
186 LifecycleError::Service(e) => &e.code,
187 LifecycleError::HostIdentity(_) => crate::error::ERROR_HOST_IDENTITY,
188 LifecycleError::NoHost => ERROR_UNAVAILABLE,
189 LifecycleError::Launch(_) | LifecycleError::LaunchTimeout => ERROR_LAUNCH_FAILED,
190 LifecycleError::RestartExhausted => ERROR_RESTART_EXHAUSTED,
191 LifecycleError::NotOwned => ERROR_NOT_OWNED,
192 LifecycleError::Released => ERROR_SESSION_RELEASED,
193 LifecycleError::StaleGeneration { .. } | LifecycleError::Unconfirmed => {
194 ERROR_STALE_GENERATION
195 }
196 LifecycleError::Process(_) => "io",
197 }
198 }
199
200 pub fn is_retryable(&self) -> bool {
203 match self {
204 LifecycleError::Service(e) => e.retryable,
205 LifecycleError::NoHost => true,
206 _ => false,
207 }
208 }
209
210 pub fn into_service_error(self) -> ServiceError {
212 let text = self.to_string();
213 match self {
214 LifecycleError::Service(e) => e,
215 LifecycleError::HostIdentity(e) => e.into(),
216 LifecycleError::NoHost => ServiceError::retryable(ERROR_UNAVAILABLE, text),
217 other => ServiceError::new(other.code().to_string(), text),
218 }
219 }
220}
221
222impl fmt::Display for LifecycleError {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 match self {
225 LifecycleError::Service(e) => fmt::Display::fmt(e, f),
226 LifecycleError::HostIdentity(e) => fmt::Display::fmt(e, f),
227 LifecycleError::NoHost => f.write_str("no live host and no launcher to start one"),
228 LifecycleError::Launch(e) => write!(f, "the host could not be launched: {e}"),
229 LifecycleError::LaunchTimeout => {
230 f.write_str("the launched host did not publish itself in time; it was stopped")
231 }
232 LifecycleError::RestartExhausted => {
233 f.write_str("the owned host exited and the restart budget is spent")
234 }
235 LifecycleError::NotOwned => f.write_str(
236 "this host was not launched by this lifecycle; attaching never implies the right to stop it",
237 ),
238 LifecycleError::Released => f.write_str("the session or the lifecycle was released"),
239 LifecycleError::StaleGeneration { issued, current } => write!(
240 f,
241 "the host generation changed (bound to epoch {issued}, now {current}); the request may have run on the previous host and is not replayed"
242 ),
243 LifecycleError::Unconfirmed => f.write_str(
244 "the host generation could not be confirmed; the reply is not delivered",
245 ),
246 LifecycleError::Process(e) => write!(f, "process operation failed: {e}"),
247 }
248 }
249}
250
251impl std::error::Error for LifecycleError {
252 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
253 match self {
254 LifecycleError::Service(e) => Some(e),
255 LifecycleError::HostIdentity(e) => Some(e),
256 LifecycleError::Launch(e) | LifecycleError::Process(e) => Some(e),
257 _ => None,
258 }
259 }
260}
261
262impl From<ServiceError> for LifecycleError {
263 fn from(e: ServiceError) -> Self {
264 LifecycleError::Service(e)
265 }
266}
267
268fn unavailable(e: io::Error) -> LifecycleError {
269 LifecycleError::Service(ServiceError::retryable(
270 ERROR_UNAVAILABLE,
271 format!("host discovery failed (try again): {e}"),
272 ))
273}
274
275const HOST_EXIT_SETTLE: Duration = Duration::from_millis(500);
282
283#[derive(Debug, Clone, Copy, PartialEq, Eq)]
285pub struct Stale {
286 pub issued: u64,
287 pub current: u64,
288}
289
290impl From<Stale> for LifecycleError {
291 fn from(s: Stale) -> Self {
292 LifecycleError::StaleGeneration {
293 issued: s.issued,
294 current: s.current,
295 }
296 }
297}
298
299#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub struct Ticket {
302 epoch: u64,
303 request: u64,
304}
305
306impl Ticket {
307 pub fn epoch(&self) -> u64 {
308 self.epoch
309 }
310
311 pub fn request(&self) -> u64 {
312 self.request
313 }
314}
315
316#[derive(Debug, Default)]
325pub struct GenerationFence {
326 epoch: AtomicU64,
327 next_request: AtomicU64,
328}
329
330impl GenerationFence {
331 pub fn new() -> Self {
332 Self::default()
333 }
334
335 pub fn epoch(&self) -> u64 {
337 self.epoch.load(Ordering::SeqCst)
338 }
339
340 pub fn advance(&self) -> u64 {
343 self.epoch.fetch_add(1, Ordering::SeqCst) + 1
344 }
345
346 pub fn issue(&self, bound_epoch: u64) -> Result<Ticket, Stale> {
349 let current = self.epoch();
350 if bound_epoch != current {
351 return Err(Stale {
352 issued: bound_epoch,
353 current,
354 });
355 }
356 Ok(Ticket {
357 epoch: bound_epoch,
358 request: self.next_request.fetch_add(1, Ordering::SeqCst) + 1,
359 })
360 }
361
362 pub fn admit_reply(&self, ticket: &Ticket) -> Result<(), Stale> {
365 let current = self.epoch();
366 if ticket.epoch != current {
367 return Err(Stale {
368 issued: ticket.epoch,
369 current,
370 });
371 }
372 Ok(())
373 }
374
375 pub fn admit_reply_from(&self, ticket: &Ticket, reply_epoch: u64) -> Result<(), Stale> {
378 if reply_epoch != ticket.epoch {
379 return Err(Stale {
380 issued: ticket.epoch,
381 current: reply_epoch,
382 });
383 }
384 self.admit_reply(ticket)
385 }
386}
387
388pub trait HostLink: Send + Sync {
394 fn call(&self, method: &str, params: Value) -> Result<Value, ServiceError>;
395 fn is_closed(&self) -> bool;
397 fn close(&self);
399}
400
401pub enum SpawnPermit {
403 Granted(Box<dyn Any + Send>),
405 Contended,
407}
408
409pub trait HostBackend: Send + Sync + 'static {
412 fn discover(&self) -> io::Result<Option<HostGeneration>>;
415
416 fn connect(&self, host: &HostGeneration) -> Result<Box<dyn HostLink>, LifecycleError>;
420
421 fn is_current(&self, host: &HostGeneration) -> io::Result<bool> {
424 Ok(self.discover()?.as_ref() == Some(host))
425 }
426
427 fn claim_spawn(&self) -> io::Result<SpawnPermit>;
429}
430
431pub struct LocalBackend {
434 client: ClientConfig,
435 expect: HostExpectation,
436}
437
438impl LocalBackend {
439 pub fn new(client: ClientConfig, expect: HostExpectation) -> Self {
443 Self { client, expect }
444 }
445
446 fn suite(&self) -> io::Result<crate::suite::Suite> {
447 self.client.resolve_suite().map_err(io::Error::other)
448 }
449}
450
451struct LocalLink(Client);
452
453impl HostLink for LocalLink {
454 fn call(&self, method: &str, params: Value) -> Result<Value, ServiceError> {
455 self.0.call(method, params)
456 }
457
458 fn is_closed(&self) -> bool {
459 self.0.is_closed()
460 }
461
462 fn close(&self) {
463 self.0.abort();
464 }
465}
466
467impl HostBackend for LocalBackend {
468 fn discover(&self) -> io::Result<Option<HostGeneration>> {
469 let suite = self.suite()?;
470 Ok(match try_adopt_or_spawn(&suite, self.client.service())? {
471 AdoptOrSpawn::Adopt(record) => Some(HostGeneration::of(&record)),
472 AdoptOrSpawn::Spawn => None,
473 })
474 }
475
476 fn connect(&self, host: &HostGeneration) -> Result<Box<dyn HostLink>, LifecycleError> {
477 let client =
478 Client::connect_expecting(&self.client, &self.expect).map_err(|e| match e {
479 ConnectError::HostIdentity(identity) => LifecycleError::HostIdentity(identity),
480 other => LifecycleError::Service(other.into_service_error()),
481 })?;
482 if HostGeneration::of(client.record()) != *host {
484 client.abort();
485 return Err(LifecycleError::Service(ServiceError::retryable(
486 ERROR_UNAVAILABLE,
487 "the host changed while connecting; discover it again",
488 )));
489 }
490 let identity_error = |found_pid: Option<u32>,
492 found_executable: Option<std::path::PathBuf>,
493 reason: HostIdentityReason| {
494 LifecycleError::HostIdentity(HostIdentityError {
495 expected: self.expect.executables().to_vec(),
496 found_pid,
497 found_executable,
498 reason,
499 })
500 };
501 match client.verified_host() {
502 Some(process) if process.pid == host.pid => {}
503 Some(process) => {
504 let error = identity_error(
505 Some(process.pid),
506 Some(process.executable.clone()),
507 HostIdentityReason::HostChanged(format!(
508 "the endpoint is served by pid {} but service.json names pid {}",
509 process.pid, host.pid
510 )),
511 );
512 client.abort();
513 return Err(error);
514 }
515 None => {
516 client.abort();
517 return Err(identity_error(
518 None,
519 None,
520 HostIdentityReason::PidUnavailable,
521 ));
522 }
523 }
524 Ok(Box::new(LocalLink(client)))
525 }
526
527 fn is_current(&self, host: &HostGeneration) -> io::Result<bool> {
528 let suite = self.suite()?;
529 Ok(try_read_record(&suite, self.client.service())?
530 .is_some_and(|r| record_process_matches(&r) && HostGeneration::of(&r) == *host))
531 }
532
533 fn claim_spawn(&self) -> io::Result<SpawnPermit> {
534 let suite = self.suite()?;
535 match SpawnClaim::acquire(&suite, self.client.service()) {
536 Ok(claim) => Ok(SpawnPermit::Granted(Box::new(claim))),
537 Err(SpawnClaimError::AlreadyHeld(_)) => Ok(SpawnPermit::Contended),
538 Err(SpawnClaimError::Io(e)) => Err(e),
539 }
540 }
541}
542
543pub type Launcher = Box<dyn FnMut() -> io::Result<OwnedChild> + Send>;
551
552pub type GracefulStop = Arc<dyn Fn(&dyn HostLink) -> Result<(), ServiceError> + Send + Sync>;
556
557#[derive(Clone)]
559pub struct LifecycleConfig {
560 startup_timeout: Duration,
561 poll_interval: Duration,
562 stop_grace: Duration,
563 restart: RestartPolicy,
564 graceful: Option<GracefulStop>,
565}
566
567impl Default for LifecycleConfig {
568 fn default() -> Self {
569 Self {
570 startup_timeout: Duration::from_secs(30),
571 poll_interval: Duration::from_millis(50),
572 stop_grace: Duration::from_secs(5),
573 restart: RestartPolicy::default(),
574 graceful: None,
575 }
576 }
577}
578
579impl LifecycleConfig {
580 pub fn new() -> Self {
581 Self::default()
582 }
583
584 #[must_use]
586 pub fn startup_timeout(mut self, timeout: Duration) -> Self {
587 self.startup_timeout = timeout;
588 self
589 }
590
591 #[must_use]
593 pub fn poll_interval(mut self, interval: Duration) -> Self {
594 self.poll_interval = interval.max(Duration::from_millis(1));
595 self
596 }
597
598 #[must_use]
601 pub fn stop_grace(mut self, grace: Duration) -> Self {
602 self.stop_grace = grace;
603 self
604 }
605
606 #[must_use]
609 pub fn restart_policy(mut self, policy: RestartPolicy) -> Self {
610 self.restart = policy;
611 self
612 }
613
614 #[must_use]
616 pub fn graceful_stop<F>(mut self, stop: F) -> Self
617 where
618 F: Fn(&dyn HostLink) -> Result<(), ServiceError> + Send + Sync + 'static,
619 {
620 self.graceful = Some(Arc::new(stop));
621 self
622 }
623}
624
625struct OwnedHost {
630 child: OwnedChild,
631 identity: AdoptedIdentity,
633 host: Option<HostGeneration>,
635}
636
637struct Slow {
638 owned: Option<OwnedHost>,
639 tracker: RestartTracker,
640 origin: Instant,
641 generation: Option<Generation>,
642 pending: Option<RestartOutcome>,
644 exhausted: bool,
645}
646
647#[derive(Clone)]
648struct Current {
649 host: HostGeneration,
650 provenance: Provenance,
651 epoch: u64,
652}
653
654struct Inner {
655 backend: Arc<dyn HostBackend>,
656 cfg: LifecycleConfig,
657 launcher: Mutex<Option<Launcher>>,
658 slow: Mutex<Slow>,
661 current: Mutex<Option<Current>>,
662 sessions: Mutex<BTreeMap<u64, String>>,
663 fence: GenerationFence,
664 next_session: AtomicU64,
665 stopped: AtomicBool,
666}
667
668pub struct DaemonLifecycle {
670 inner: Arc<Inner>,
671}
672
673#[derive(Debug, Clone, PartialEq, Eq)]
675pub struct ReleaseReport {
676 pub label: String,
677 pub provenance: Provenance,
678 pub remaining_sessions: usize,
680}
681
682#[derive(Debug, Clone, PartialEq, Eq)]
684pub struct ShutdownReport {
685 pub graceful_requested: bool,
687 pub graceful_acknowledged: bool,
689 pub killed: bool,
691 pub exit_code: Option<i32>,
693}
694
695impl DaemonLifecycle {
696 pub fn attach_or_launch(
702 backend: Arc<dyn HostBackend>,
703 launcher: Option<Launcher>,
704 config: LifecycleConfig,
705 ) -> Result<Self, LifecycleError> {
706 let tracker = RestartTracker::new(config.restart);
707 let inner = Arc::new(Inner {
708 backend,
709 cfg: config,
710 launcher: Mutex::new(launcher),
711 slow: Mutex::new(Slow {
712 owned: None,
713 tracker,
714 origin: Instant::now(),
715 generation: None,
716 pending: None,
717 exhausted: false,
718 }),
719 current: Mutex::new(None),
720 sessions: Mutex::new(BTreeMap::new()),
721 fence: GenerationFence::new(),
722 next_session: AtomicU64::new(1),
723 stopped: AtomicBool::new(false),
724 });
725 inner.ensure_host()?;
726 Ok(Self { inner })
727 }
728
729 pub fn attach_or_launch_local(
731 client: ClientConfig,
732 expect: HostExpectation,
733 launcher: Option<Launcher>,
734 config: LifecycleConfig,
735 ) -> Result<Self, LifecycleError> {
736 Self::attach_or_launch(
737 Arc::new(LocalBackend::new(client, expect)),
738 launcher,
739 config,
740 )
741 }
742
743 pub fn provenance(&self) -> Option<Provenance> {
745 lock(&self.inner.current).as_ref().map(|c| c.provenance)
746 }
747
748 pub fn host(&self) -> Option<HostGeneration> {
750 lock(&self.inner.current).as_ref().map(|c| c.host.clone())
751 }
752
753 pub fn epoch(&self) -> u64 {
755 self.inner.fence.epoch()
756 }
757
758 pub fn session_count(&self) -> usize {
760 lock(&self.inner.sessions).len()
761 }
762
763 pub fn reset_restart_budget(&self) {
765 let mut slow = lock(&self.inner.slow);
766 slow.tracker.reset_budget();
767 slow.pending = None;
768 slow.exhausted = false;
769 }
770
771 pub fn acquire_session(&self, label: &str) -> Result<Session, LifecycleError> {
775 let current = self.inner.ensure_host()?;
776 let link = self.inner.backend.connect(¤t.host)?;
777 match self.inner.backend.is_current(¤t.host) {
779 Ok(true) => {}
780 Ok(false) => {
781 link.close();
782 self.inner.host_left(¤t.host);
783 return Err(LifecycleError::StaleGeneration {
784 issued: current.epoch,
785 current: self.inner.fence.epoch(),
786 });
787 }
788 Err(_) => {
789 link.close();
790 return Err(LifecycleError::Unconfirmed);
791 }
792 }
793 let id = self.inner.next_session.fetch_add(1, Ordering::SeqCst);
794 lock(&self.inner.sessions).insert(id, label.to_string());
795 Ok(Session {
796 inner: self.inner.clone(),
797 id,
798 label: label.to_string(),
799 provenance: current.provenance,
800 host: current.host,
801 epoch: current.epoch,
802 link,
803 released: AtomicBool::new(false),
804 })
805 }
806
807 pub fn release_session(&self, session: Session) -> ReleaseReport {
811 session.finish()
812 }
813
814 pub fn shutdown_owned(&self) -> Result<ShutdownReport, LifecycleError> {
823 let inner = &self.inner;
824 let mut slow = lock(&inner.slow);
825 let identity_state = match slow.owned.as_ref() {
827 None => return Err(LifecycleError::NotOwned),
828 Some(owned) => owned.identity.verify().map_err(LifecycleError::Process)?,
829 };
830 if identity_state == IdentityCheck::Replaced {
831 return Err(LifecycleError::Process(io::Error::other(
832 "the launched pid now names a different process; nothing was signalled",
833 )));
834 }
835 let Some(mut owned) = slow.owned.take() else {
836 return Err(LifecycleError::NotOwned);
837 };
838 inner.stopped.store(true, Ordering::SeqCst);
839 slow.tracker.shutdown();
840 slow.generation = None;
841
842 let mut graceful_requested = false;
843 let mut graceful_acknowledged = false;
844 if let (Some(stop), Some(host)) = (inner.cfg.graceful.as_ref(), owned.host.as_ref()) {
845 if matches!(inner.backend.is_current(host), Ok(true)) {
847 if let Ok(link) = inner.backend.connect(host) {
848 graceful_requested = true;
849 graceful_acknowledged = stop(&*link).is_ok();
850 link.close();
851 }
852 }
853 }
854 let outcome = owned
855 .child
856 .wait_then_kill(inner.cfg.stop_grace)
857 .map_err(LifecycleError::Process)?;
858 let (killed, exit_code) = match outcome {
859 WaitOutcome::Exited(status) => (false, status.code()),
860 WaitOutcome::Terminated(status) => (true, status.code()),
861 };
862 drop(owned);
863 drop(slow);
864 let mut current = lock(&inner.current);
866 if current.take().is_some() {
867 inner.fence.advance();
868 }
869 Ok(ShutdownReport {
870 graceful_requested,
871 graceful_acknowledged,
872 killed,
873 exit_code,
874 })
875 }
876}
877
878impl Inner {
879 fn bind(&self, to: Option<(HostGeneration, Provenance)>) -> Option<Current> {
880 let mut current = lock(&self.current);
881 let unchanged = match (current.as_ref(), to.as_ref()) {
882 (Some(c), Some((host, provenance))) => c.host == *host && c.provenance == *provenance,
883 (None, None) => true,
884 _ => false,
885 };
886 if unchanged {
887 return current.clone();
888 }
889 let epoch = self.fence.advance();
890 *current = to.map(|(host, provenance)| Current {
891 host,
892 provenance,
893 epoch,
894 });
895 current.clone()
896 }
897
898 fn host_left(&self, host: &HostGeneration) {
901 let mut current = lock(&self.current);
902 if current.as_ref().is_some_and(|c| c.host == *host) {
903 *current = None;
904 self.fence.advance();
905 }
906 }
907
908 fn release(&self, id: u64) -> usize {
909 let mut sessions = lock(&self.sessions);
910 sessions.remove(&id);
911 sessions.len()
912 }
913
914 fn owned_exited(&self, host: &HostGeneration) -> bool {
919 let mut slow = match self.slow.try_lock() {
920 Ok(guard) => guard,
921 Err(std::sync::TryLockError::Poisoned(e)) => e.into_inner(),
922 Err(std::sync::TryLockError::WouldBlock) => return false,
923 };
924 match slow.owned.as_mut() {
925 Some(owned) if owned.host.as_ref() == Some(host) => {
926 !matches!(owned.child.try_wait(), Ok(None))
927 }
928 _ => false,
929 }
930 }
931
932 fn ensure_host(&self) -> Result<Current, LifecycleError> {
934 let mut slow = lock(&self.slow);
935 if self.stopped.load(Ordering::SeqCst) {
936 return Err(LifecycleError::Released);
937 }
938 let deadline = Instant::now() + self.cfg.startup_timeout;
939 self.reap_exited_owned(&mut slow);
940 match self.backend.discover().map_err(unavailable)? {
941 Some(host) => self.adopt_found(&mut slow, host),
942 None => {
943 self.bind(None);
944 self.launch(&mut slow, deadline)
945 }
946 }
947 }
948
949 fn reap_exited_owned(&self, slow: &mut Slow) {
951 let exited = match slow.owned.as_mut() {
952 Some(owned) => match owned.child.try_wait() {
953 Ok(None) => false,
954 Ok(Some(_)) => true,
955 Err(_) => {
956 let _ = owned.child.terminate_tree();
958 true
959 }
960 },
961 None => false,
962 };
963 if exited {
964 slow.owned = None;
965 if let Some(generation) = slow.generation.take() {
966 let at = slow.origin.elapsed();
967 slow.pending = Some(slow.tracker.exited(generation, at));
968 }
969 }
970 }
971
972 fn adopt_found(
974 &self,
975 slow: &mut Slow,
976 host: HostGeneration,
977 ) -> Result<Current, LifecycleError> {
978 if let Some(current) = lock(&self.current).clone() {
979 if current.host == host {
980 return Ok(current);
981 }
982 }
983 let link = self.backend.connect(&host)?;
985 link.close();
986 let ours = slow
989 .owned
990 .as_ref()
991 .is_some_and(|owned| owned.child.id() == host.pid);
992 if ours {
993 if let Some(owned) = slow.owned.as_mut() {
994 owned.host = Some(host.clone());
995 }
996 } else if slow.owned.is_some() {
997 self.retire_owned(slow);
1000 }
1001 let provenance = if ours {
1002 Provenance::Owned
1003 } else {
1004 Provenance::Attached
1005 };
1006 self.bind(Some((host, provenance)))
1007 .ok_or(LifecycleError::NoHost)
1008 }
1009
1010 fn retire_owned(&self, slow: &mut Slow) {
1012 if let Some(mut owned) = slow.owned.take() {
1013 stop_launched(&mut owned, self.cfg.stop_grace);
1014 }
1015 slow.generation = None;
1016 }
1017
1018 fn fail_owned(&self, slow: &mut Slow) {
1020 if let Some(mut owned) = slow.owned.take() {
1021 stop_launched(&mut owned, self.cfg.stop_grace);
1022 }
1023 let at = slow.origin.elapsed();
1024 if let Some(generation) = slow.generation.take() {
1025 slow.pending = Some(slow.tracker.exited(generation, at));
1026 } else {
1027 slow.pending = Some(slow.tracker.request_restart(at));
1028 }
1029 }
1030
1031 fn charge_failed_launch(&self, slow: &mut Slow) {
1032 let at = slow.origin.elapsed();
1033 slow.pending = Some(slow.tracker.request_restart(at));
1034 }
1035
1036 fn launch(&self, slow: &mut Slow, deadline: Instant) -> Result<Current, LifecycleError> {
1037 if slow.exhausted {
1038 return Err(LifecycleError::RestartExhausted);
1039 }
1040 if lock(&self.launcher).is_none() {
1041 return Err(LifecycleError::NoHost);
1042 }
1043 if let Some(outcome) = slow.pending.take() {
1044 self.honour(slow, outcome, deadline)?;
1045 }
1046 let mut claim: Option<Box<dyn Any + Send>> = None;
1047 if slow.owned.is_none() {
1048 loop {
1049 match self.backend.claim_spawn().map_err(LifecycleError::Launch)? {
1050 SpawnPermit::Granted(held) => {
1051 claim = Some(held);
1052 break;
1053 }
1054 SpawnPermit::Contended => {
1055 if let Some(host) = self.backend.discover().map_err(unavailable)? {
1056 return self.adopt_found(slow, host);
1057 }
1058 if Instant::now() >= deadline {
1059 return Err(LifecycleError::LaunchTimeout);
1060 }
1061 std::thread::sleep(self.cfg.poll_interval);
1062 }
1063 }
1064 }
1065 if let Some(host) = self.backend.discover().map_err(unavailable)? {
1067 return self.adopt_found(slow, host);
1068 }
1069 self.spawn_child(slow)?;
1070 }
1071 let result = self.await_publication(slow, deadline);
1072 drop(claim);
1073 result
1074 }
1075
1076 fn honour(
1078 &self,
1079 slow: &mut Slow,
1080 outcome: RestartOutcome,
1081 deadline: Instant,
1082 ) -> Result<(), LifecycleError> {
1083 let at = slow.origin.elapsed();
1084 let outcome = match outcome {
1085 RestartOutcome::AwaitingDemand => slow.tracker.request_restart(at),
1087 other => other,
1088 };
1089 match outcome {
1090 RestartOutcome::Restart(permit) => {
1091 let delay = permit.delay();
1092 if delay > deadline.saturating_duration_since(Instant::now()) {
1093 slow.pending = Some(RestartOutcome::Restart(permit));
1094 return Err(LifecycleError::Service(ServiceError::retryable(
1095 "restart_backoff",
1096 format!(
1097 "the next start of the owned host waits {} ms (restart backoff)",
1098 delay.as_millis()
1099 ),
1100 )));
1101 }
1102 std::thread::sleep(delay);
1103 if slow.tracker.permit_valid(&permit, slow.origin.elapsed()) {
1104 Ok(())
1105 } else {
1106 Err(LifecycleError::NoHost)
1107 }
1108 }
1109 RestartOutcome::Exhausted => {
1110 slow.exhausted = true;
1111 Err(LifecycleError::RestartExhausted)
1112 }
1113 RestartOutcome::AwaitingDemand
1114 | RestartOutcome::Stopped
1115 | RestartOutcome::IgnoredStale => Err(LifecycleError::NoHost),
1116 }
1117 }
1118
1119 fn spawn_child(&self, slow: &mut Slow) -> Result<(), LifecycleError> {
1120 let launched = {
1121 let mut launcher = lock(&self.launcher);
1122 match launcher.as_mut() {
1123 Some(launch) => launch(),
1124 None => return Err(LifecycleError::NoHost),
1125 }
1126 };
1127 let mut child = match launched {
1128 Ok(child) => child,
1129 Err(e) => {
1130 self.charge_failed_launch(slow);
1131 return Err(LifecycleError::Launch(e));
1132 }
1133 };
1134 let Some(pid) = NonZeroU32::new(child.id()) else {
1135 let _ = child.terminate_tree();
1136 self.charge_failed_launch(slow);
1137 return Err(LifecycleError::Launch(io::Error::other(
1138 "the launcher returned a child without a process id",
1139 )));
1140 };
1141 let identity = match AdoptedIdentity::capture(pid) {
1142 Ok(Some(identity)) => identity,
1143 Ok(None) => {
1144 let _ = child.terminate_tree();
1145 self.charge_failed_launch(slow);
1146 return Err(LifecycleError::Launch(io::Error::other(
1147 "the launched process exited before its identity could be recorded",
1148 )));
1149 }
1150 Err(e) => {
1151 let _ = child.terminate_tree();
1152 self.charge_failed_launch(slow);
1153 return Err(LifecycleError::Process(e));
1154 }
1155 };
1156 slow.generation = Some(slow.tracker.started(slow.origin.elapsed()));
1157 slow.owned = Some(OwnedHost {
1158 child,
1159 identity,
1160 host: None,
1161 });
1162 Ok(())
1163 }
1164
1165 fn await_publication(
1167 &self,
1168 slow: &mut Slow,
1169 deadline: Instant,
1170 ) -> Result<Current, LifecycleError> {
1171 loop {
1172 let pid = match slow.owned.as_mut() {
1173 Some(owned) => {
1174 if !matches!(owned.child.try_wait(), Ok(None)) {
1175 self.fail_owned(slow);
1176 return Err(LifecycleError::Launch(io::Error::other(
1177 "the launched host exited before it published itself",
1178 )));
1179 }
1180 owned.child.id()
1181 }
1182 None => return Err(LifecycleError::NoHost),
1183 };
1184 match self.backend.discover().map_err(unavailable)? {
1185 Some(host) if host.pid == pid => {
1186 if let Err(e) = self.backend.connect(&host).map(|link| link.close()) {
1187 self.fail_owned(slow);
1189 return Err(e);
1190 }
1191 if let Some(owned) = slow.owned.as_mut() {
1192 owned.host = Some(host.clone());
1193 }
1194 if let Some(generation) = slow.generation {
1195 slow.tracker.ready(generation);
1196 }
1197 return self
1198 .bind(Some((host, Provenance::Owned)))
1199 .ok_or(LifecycleError::NoHost);
1200 }
1201 Some(host) => {
1202 self.retire_owned(slow);
1205 return self.adopt_found(slow, host);
1206 }
1207 None => {}
1208 }
1209 if Instant::now() >= deadline {
1210 self.fail_owned(slow);
1211 return Err(LifecycleError::LaunchTimeout);
1212 }
1213 std::thread::sleep(self.cfg.poll_interval);
1214 }
1215 }
1216}
1217
1218fn stop_launched(owned: &mut OwnedHost, grace: Duration) {
1221 if let Ok(IdentityCheck::Replaced) = owned.identity.verify() {
1222 return;
1223 }
1224 let _ = owned.child.wait_then_kill(grace);
1225}
1226
1227pub struct Session {
1230 inner: Arc<Inner>,
1231 id: u64,
1232 label: String,
1233 provenance: Provenance,
1234 host: HostGeneration,
1235 epoch: u64,
1236 link: Box<dyn HostLink>,
1237 released: AtomicBool,
1238}
1239
1240impl Session {
1241 pub fn provenance(&self) -> Provenance {
1242 self.provenance
1243 }
1244
1245 pub fn host(&self) -> &HostGeneration {
1246 &self.host
1247 }
1248
1249 pub fn epoch(&self) -> u64 {
1251 self.epoch
1252 }
1253
1254 pub fn label(&self) -> &str {
1255 &self.label
1256 }
1257
1258 pub fn is_valid(&self) -> bool {
1261 !self.released.load(Ordering::SeqCst)
1262 && !self.inner.stopped.load(Ordering::SeqCst)
1263 && !self.link.is_closed()
1264 && self.inner.fence.epoch() == self.epoch
1265 }
1266
1267 pub fn call(&self, method: &str, params: Value) -> Result<Value, LifecycleError> {
1272 if self.released.load(Ordering::SeqCst) || self.inner.stopped.load(Ordering::SeqCst) {
1273 return Err(LifecycleError::Released);
1274 }
1275 self.confirm_current()?;
1276 let ticket = self.inner.fence.issue(self.epoch)?;
1277 let reply = self.link.call(method, params);
1278 if reply.is_err() && self.link.is_closed() {
1282 let settle = Instant::now() + HOST_EXIT_SETTLE;
1283 while Instant::now() < settle {
1284 self.confirm_current()?;
1285 std::thread::sleep(Duration::from_millis(20));
1286 }
1287 }
1288 self.confirm_current()?;
1290 self.inner.fence.admit_reply(&ticket)?;
1291 reply.map_err(LifecycleError::Service)
1292 }
1293
1294 fn confirm_current(&self) -> Result<(), LifecycleError> {
1295 let left = if self.provenance == Provenance::Owned && self.inner.owned_exited(&self.host) {
1296 true
1297 } else {
1298 match self.inner.backend.is_current(&self.host) {
1299 Ok(true) => false,
1300 Ok(false) => true,
1301 Err(_) => return Err(LifecycleError::Unconfirmed),
1302 }
1303 };
1304 if left {
1305 self.inner.host_left(&self.host);
1306 return Err(LifecycleError::StaleGeneration {
1307 issued: self.epoch,
1308 current: self.inner.fence.epoch(),
1309 });
1310 }
1311 Ok(())
1312 }
1313
1314 fn finish(&self) -> ReleaseReport {
1315 let first = !self.released.swap(true, Ordering::SeqCst);
1316 let remaining = if first {
1317 self.inner.release(self.id)
1318 } else {
1319 lock(&self.inner.sessions).len()
1320 };
1321 self.link.close();
1322 ReleaseReport {
1323 label: self.label.clone(),
1324 provenance: self.provenance,
1325 remaining_sessions: remaining,
1326 }
1327 }
1328}
1329
1330impl Drop for Session {
1331 fn drop(&mut self) {
1332 let _ = self.finish();
1333 }
1334}