1use crate::contract::{new_context, Caller};
2pub use crate::contract::{CallContext, Emit, Handler};
3use crate::discovery::{record_process_matches, try_lock_exclusive, LockedFile};
4use crate::error::{ServiceError, ERROR_RESPONSE_TOO_LARGE};
5use crate::record::*;
6use crate::suite::{make_private_dir, valid_service_name, Suite, SuiteError};
7use crate::takeover_watch::{self, Bell, Wake, TAKEOVER_CHECK_INTERVAL};
8use crate::transport::{
9 self, pid_alive, probe_endpoint, try_endpoint_for, Accepted, Conn, Listener,
10 DEFAULT_MAX_PIPE_INSTANCES,
11};
12use rightkit_sidecar::{read_json_line, write_json_line, FrameError};
13use serde_json::{json, Value};
14use std::collections::HashMap;
15use std::io::{BufReader, Write};
16use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
17use std::sync::{Arc, Condvar, Mutex};
18use std::time::{Duration, Instant, SystemTime};
19
20pub const DEFAULT_FRAME_LIMIT: usize = 16 * 1024 * 1024;
21
22pub const DEFAULT_WRITE_TIMEOUT: Duration = Duration::from_secs(120);
25
26pub const DEFAULT_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(5);
30
31pub const DEFAULT_RESERVED_SLOTS: usize = 2;
33
34#[derive(Debug, Clone, PartialEq)]
36pub enum ServiceEvent {
37 Started {
38 service: String,
39 endpoint: String,
40 rank: u8,
41 },
42 ConnectionOpened {
43 conn: u64,
44 app: String,
45 },
46 ConnectionRejected {
47 app: String,
48 reason: String,
49 },
50 PeerRejected {
54 reason: String,
55 },
56 TableFull {
69 limit: usize,
70 reserved: usize,
71 pre_handshake: usize,
72 attached: usize,
73 peer_pid: Option<u32>,
74 },
75 Denied {
76 conn: u64,
77 app: String,
78 method: String,
79 },
80 HandoffRequested {
81 by_role: String,
82 by_rank: u8,
83 },
84 FrameTooLarge {
88 conn: u64,
89 method: String,
90 kind: &'static str,
91 limit: usize,
92 },
93 Released,
94}
95
96pub type EventFn = Arc<dyn Fn(&ServiceEvent) + Send + Sync>;
98
99pub struct ServiceConfig {
100 pub service: String,
101 pub role: String,
102 pub rank: u8,
103 pub version: String,
104 pub allowlist: Allowlist,
105 pub frame_limit: usize,
106 pub takeover_timeout: Duration,
108 pub handoff_grace: Duration,
110 pub on_event: Option<EventFn>,
111 pub known_roles: std::collections::BTreeMap<String, u8>,
115 pub endpoint: Option<String>,
118 pub private_secret: Option<crate::private::PrivateSecret>,
120}
121
122impl ServiceConfig {
123 pub fn new(service: &str, role: &str, rank: u8, version: &str, allowlist: Allowlist) -> Self {
124 Self {
125 service: service.into(),
126 role: role.into(),
127 rank,
128 version: version.into(),
129 allowlist,
130 frame_limit: DEFAULT_FRAME_LIMIT,
131 takeover_timeout: Duration::from_secs(60),
132 handoff_grace: Duration::from_secs(30),
133 on_event: None,
134 known_roles: Default::default(),
135 endpoint: None,
136 private_secret: None,
137 }
138 }
139
140 pub fn private_secret(mut self, secret: crate::private::PrivateSecret) -> Self {
145 self.private_secret = Some(secret);
146 self
147 }
148
149 pub fn with_methods(mut self, table: &crate::contract::MethodTable) -> Self {
152 self.allowlist = table.allowlist();
153 self
154 }
155}
156
157#[derive(Debug, Clone, Copy, PartialEq, Eq)]
160pub struct HostOptions {
161 max_pipe_instances: u32,
162 write_timeout: Option<Duration>,
163 max_message: usize,
164 max_connections: usize,
165 reserved_slots: usize,
166 supervisor: SupervisorPid,
167 handshake_timeout: Option<Duration>,
168}
169
170pub const DEFAULT_MAX_CONNECTIONS: usize = 64;
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
175enum SupervisorPid {
176 Parent,
178 Pid(u32),
179 Nobody,
180}
181
182impl Default for HostOptions {
183 fn default() -> Self {
184 Self {
185 max_pipe_instances: DEFAULT_MAX_PIPE_INSTANCES,
186 write_timeout: Some(DEFAULT_WRITE_TIMEOUT),
187 max_message: crate::chunk::DEFAULT_MAX_MESSAGE,
188 max_connections: DEFAULT_MAX_CONNECTIONS,
189 reserved_slots: DEFAULT_RESERVED_SLOTS,
190 supervisor: SupervisorPid::Parent,
191 handshake_timeout: Some(DEFAULT_HANDSHAKE_TIMEOUT),
192 }
193 }
194}
195
196impl HostOptions {
197 pub fn new() -> Self {
198 Self::default()
199 }
200
201 #[must_use]
208 pub fn max_pipe_instances(mut self, n: u32) -> Self {
209 self.max_pipe_instances = n.clamp(1, 254);
210 self
211 }
212
213 pub fn get_max_pipe_instances(&self) -> u32 {
215 self.max_pipe_instances
216 }
217
218 #[must_use]
227 pub fn write_timeout(mut self, bound: Option<Duration>) -> Self {
228 self.write_timeout = bound.map(|d| d.max(Duration::from_millis(1)));
229 self
230 }
231
232 pub fn get_write_timeout(&self) -> Option<Duration> {
234 self.write_timeout
235 }
236
237 #[must_use]
247 pub fn max_message(mut self, bytes: usize) -> Self {
248 self.max_message = bytes;
249 self
250 }
251
252 pub fn get_max_message(&self) -> usize {
254 self.max_message
255 }
256
257 #[must_use]
264 pub fn max_connections(mut self, n: usize) -> Self {
265 self.max_connections = n.max(1);
266 self
267 }
268
269 pub fn get_max_connections(&self) -> usize {
271 self.max_connections
272 }
273
274 #[must_use]
284 pub fn reserved_slots(mut self, n: usize) -> Self {
285 self.reserved_slots = n;
286 self
287 }
288
289 pub fn get_reserved_slots(&self) -> usize {
291 self.reserved_slots
292 }
293
294 #[must_use]
299 pub fn supervisor_pid(mut self, pid: Option<u32>) -> Self {
300 self.supervisor = match pid {
301 Some(pid) => SupervisorPid::Pid(pid),
302 None => SupervisorPid::Nobody,
303 };
304 self
305 }
306
307 pub fn get_supervisor_pid(&self) -> Option<u32> {
310 match self.supervisor {
311 SupervisorPid::Pid(pid) => Some(pid),
312 SupervisorPid::Nobody => None,
313 #[cfg(unix)]
314 SupervisorPid::Parent => Some(std::os::unix::process::parent_id()),
315 #[cfg(not(unix))]
316 SupervisorPid::Parent => None,
317 }
318 }
319
320 #[must_use]
330 pub fn handshake_timeout(mut self, bound: Option<Duration>) -> Self {
331 self.handshake_timeout = bound.map(|d| d.max(Duration::from_millis(1)));
332 self
333 }
334
335 pub fn get_handshake_timeout(&self) -> Option<Duration> {
337 self.handshake_timeout
338 }
339}
340
341#[derive(Debug)]
342pub enum HostError {
343 Held(ServiceRecord),
345 TakeoverTimeout,
347 InvalidName(String),
348 Suite(SuiteError),
349 Io(std::io::Error),
350 Endpoint(crate::transport::EndpointError),
352}
353impl std::fmt::Display for HostError {
354 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355 match self {
356 HostError::Held(r) => write!(
357 f,
358 "{} is already hosted by {} (rank {}, pid {})",
359 r.service, r.role, r.rank, r.pid
360 ),
361 HostError::TakeoverTimeout => write!(f, "incumbent host did not release in time"),
362 HostError::InvalidName(s) => write!(f, "invalid service name {s:?}"),
363 HostError::Suite(e) => write!(f, "{e}"),
364 HostError::Io(e) => write!(f, "{e}"),
365 HostError::Endpoint(e) => write!(f, "{e}"),
366 }
367 }
368}
369impl std::error::Error for HostError {}
370impl From<std::io::Error> for HostError {
371 fn from(e: std::io::Error) -> Self {
372 HostError::Io(e)
373 }
374}
375impl From<SuiteError> for HostError {
376 fn from(e: SuiteError) -> Self {
377 HostError::Suite(e)
378 }
379}
380
381fn resolve_rank(cfg: &ServiceConfig, role: &str, rank: u8) -> u8 {
382 crate::discovery::resolve_rank(role, rank, &cfg.known_roles).unwrap_or(RANK_UNKNOWN)
383}
384
385struct Shared {
386 suite: Suite,
387 cfg: ServiceConfig,
388 handler: Arc<dyn Handler>,
389 listener: Listener,
390 election: Mutex<Option<LockedFile>>,
392 record: ServiceRecord,
393 stop: AtomicBool,
394 handoff: Arc<AtomicBool>,
395 released: Mutex<bool>,
396 released_cv: Condvar,
397 inflight: AtomicUsize,
398 next_conn: AtomicU64,
399 conns: Mutex<HashMap<u64, Arc<dyn Fn() + Send + Sync>>>,
400 bell: Arc<Bell>,
402 watch: WatchTuning,
403 write_timeout: Option<Duration>,
404 max_message: usize,
405 max_connections: usize,
406 live_conns: Arc<AtomicUsize>,
408 reserved_slots: usize,
410 supervisor: Option<u32>,
411 reserved_conns: Arc<AtomicUsize>,
413 pre_handshake: Arc<AtomicUsize>,
415 handshake_timeout: Option<Duration>,
416 fails: Mutex<crate::fail_limit::FailLimiter>,
418}
419
420struct ForgetFails {
422 s: Arc<Shared>,
423 conn: u64,
424}
425
426impl Drop for ForgetFails {
427 fn drop(&mut self) {
428 self.s
429 .fails
430 .lock()
431 .unwrap_or_else(|e| e.into_inner())
432 .forget(self.conn, Instant::now());
433 }
434}
435
436struct ConnSlot(Arc<AtomicUsize>);
438
439impl Drop for ConnSlot {
440 fn drop(&mut self) {
441 self.0.fetch_sub(1, Ordering::SeqCst);
442 }
443}
444
445struct PreHandshake {
448 count: Arc<AtomicUsize>,
449 pending: bool,
450}
451
452impl PreHandshake {
453 fn enter(count: &Arc<AtomicUsize>) -> Self {
454 count.fetch_add(1, Ordering::SeqCst);
455 Self {
456 count: count.clone(),
457 pending: true,
458 }
459 }
460
461 fn settle(&mut self) {
463 if std::mem::take(&mut self.pending) {
464 self.count.fetch_sub(1, Ordering::SeqCst);
465 }
466 }
467}
468
469impl Drop for PreHandshake {
470 fn drop(&mut self) {
471 self.settle();
472 }
473}
474
475struct DeadlineWatch {
483 state: Mutex<WatchState>,
484 cv: Condvar,
485}
486
487struct WatchState {
488 until: Option<Instant>,
490 done: bool,
491}
492
493struct FinishOnDrop(Arc<DeadlineWatch>);
494impl Drop for FinishOnDrop {
495 fn drop(&mut self) {
496 self.0.finish();
497 }
498}
499
500impl DeadlineWatch {
501 fn start(expired: Arc<dyn Fn() + Send + Sync>) -> Arc<DeadlineWatch> {
502 let w = Arc::new(DeadlineWatch {
503 state: Mutex::new(WatchState {
504 until: None,
505 done: false,
506 }),
507 cv: Condvar::new(),
508 });
509 let watcher = w.clone();
510 std::thread::spawn(move || {
511 let mut g = watcher.state.lock().unwrap_or_else(|e| e.into_inner());
512 loop {
513 if g.done {
514 return;
515 }
516 match g.until {
517 None => g = watcher.cv.wait(g).unwrap_or_else(|e| e.into_inner()),
518 Some(t) => {
519 let left = t.saturating_duration_since(Instant::now());
520 if left.is_zero() {
521 g.until = None;
522 drop(g);
523 expired();
524 return;
525 }
526 g = watcher
527 .cv
528 .wait_timeout(g, left)
529 .unwrap_or_else(|e| e.into_inner())
530 .0;
531 }
532 }
533 }
534 });
535 w
536 }
537 fn set(&self, until: Option<Instant>) {
538 self.state.lock().unwrap_or_else(|e| e.into_inner()).until = until;
539 self.cv.notify_all();
540 }
541 fn finish(&self) {
542 self.state.lock().unwrap_or_else(|e| e.into_inner()).done = true;
543 self.cv.notify_all();
544 }
545}
546
547#[derive(Clone, Copy)]
550struct WatchTuning {
551 interval: Duration,
553 alive: fn(u32) -> bool,
555}
556
557impl WatchTuning {
558 const PRODUCTION: WatchTuning = WatchTuning {
559 interval: TAKEOVER_CHECK_INTERVAL,
560 alive: pid_alive,
561 };
562}
563
564impl Shared {
565 fn event(&self, e: ServiceEvent) {
566 if let Some(f) = &self.cfg.on_event {
567 f(&e);
568 }
569 }
570
571 fn release(&self) {
572 {
573 let mut r = self.released.lock().unwrap_or_else(|e| e.into_inner());
574 if *r {
575 return;
576 }
577 *r = true;
578 }
579 self.stop.store(true, Ordering::SeqCst);
580 self.bell.stop();
582 if read_record(&self.suite, &self.cfg.service).is_some_and(|r| r.pid == self.record.pid) {
584 let _ = std::fs::remove_file(record_path(&self.suite, &self.cfg.service));
585 }
586 self.listener.wake();
587 self.listener.cleanup();
588 for (_, close) in self.conns.lock().unwrap_or_else(|e| e.into_inner()).drain() {
589 close();
590 }
591 self.election
592 .lock()
593 .unwrap_or_else(|e| e.into_inner())
594 .take();
595 self.event(ServiceEvent::Released);
596 self.released_cv.notify_all();
597 }
598}
599
600#[derive(Debug, Clone, Copy, PartialEq)]
602pub struct ServiceStatus {
603 pub connections: usize,
605 pub inflight: usize,
607 pub handoff_requested: bool,
609 pub released: bool,
611 pub process: Option<rightkit_child_process::ProcessUsage>,
614}
615
616pub struct Hosted {
618 shared: Arc<Shared>,
619}
620
621impl Hosted {
622 pub fn record(&self) -> &ServiceRecord {
623 &self.shared.record
624 }
625 pub fn handoff_requested(&self) -> bool {
626 self.shared.handoff.load(Ordering::SeqCst)
627 }
628 pub fn inflight(&self) -> usize {
629 self.shared.inflight.load(Ordering::SeqCst)
630 }
631 pub fn status(&self) -> ServiceStatus {
634 ServiceStatus {
635 connections: self
636 .shared
637 .conns
638 .lock()
639 .unwrap_or_else(|e| e.into_inner())
640 .len(),
641 inflight: self.shared.inflight.load(Ordering::SeqCst),
642 handoff_requested: self.shared.handoff.load(Ordering::SeqCst),
643 released: *self
644 .shared
645 .released
646 .lock()
647 .unwrap_or_else(|e| e.into_inner()),
648 process: rightkit_child_process::process_usage().ok(),
649 }
650 }
651 pub fn wait_released(&self, timeout: Duration) -> bool {
653 let g = self
654 .shared
655 .released
656 .lock()
657 .unwrap_or_else(|e| e.into_inner());
658 let (g, _) = self
659 .shared
660 .released_cv
661 .wait_timeout_while(g, timeout, |r| !*r)
662 .unwrap_or_else(|e| e.into_inner());
663 *g
664 }
665 pub fn shutdown(&self) {
666 self.shared.release();
667 }
668}
669
670impl Drop for Hosted {
671 fn drop(&mut self) {
672 self.shared.release();
673 }
674}
675
676#[allow(clippy::result_large_err)]
682pub fn host(
683 suite: &Suite,
684 cfg: ServiceConfig,
685 handler: Arc<dyn Handler>,
686) -> Result<Hosted, HostError> {
687 host_with_options(suite, cfg, handler, HostOptions::default())
688}
689
690#[allow(clippy::result_large_err)]
692pub fn host_with_options(
693 suite: &Suite,
694 cfg: ServiceConfig,
695 handler: Arc<dyn Handler>,
696 options: HostOptions,
697) -> Result<Hosted, HostError> {
698 start_host(suite, cfg, handler, options, WatchTuning::PRODUCTION)
699}
700
701#[allow(clippy::result_large_err)]
702fn start_host(
703 suite: &Suite,
704 cfg: ServiceConfig,
705 handler: Arc<dyn Handler>,
706 options: HostOptions,
707 watch: WatchTuning,
708) -> Result<Hosted, HostError> {
709 if !valid_service_name(&cfg.service) {
710 return Err(HostError::InvalidName(cfg.service.clone()));
711 }
712 let dir = suite.service_dir(&cfg.service);
713 make_private_dir(&dir)?;
714 let me = std::process::id();
715 let deadline = Instant::now() + cfg.takeover_timeout;
716 let mut requested = false;
717 let lock_path = dir.join("election.lock");
718 let endpoint = match cfg.endpoint.clone() {
719 Some(e) => e,
720 None => try_endpoint_for(suite, &cfg.service).map_err(HostError::Endpoint)?,
721 };
722 let mut backoff = Duration::from_millis(5);
723 let (listener, election) = loop {
724 let election = try_lock_exclusive(&lock_path)?;
728 let current = try_read_record(suite, &cfg.service);
731 let unreadable = current
732 .as_ref()
733 .err()
734 .map(|e| std::io::Error::new(e.kind(), e.to_string()));
735 match current {
736 Ok(Some(rec)) if record_process_matches(&rec) && probe_endpoint(&rec.endpoint) => {
737 let their = resolve_rank(&cfg, &rec.role, rec.rank);
738 if their == RANK_UNKNOWN || cfg.rank >= their {
739 return Err(HostError::Held(rec));
740 }
741 if !requested {
742 let t = TakeoverRequest {
743 role: cfg.role.clone(),
744 rank: cfg.rank,
745 pid: me,
746 requested_at_ms: now_ms(),
747 };
748 let request_path = takeover_path(suite, &cfg.service);
749 write_atomic(&request_path, &serde_json::to_vec(&t).unwrap_or_default())?;
750 takeover_watch::poke(&request_path);
752 requested = true;
753 }
754 }
755 Ok(_) if election.is_some() => {
756 let _ = std::fs::remove_file(record_path(suite, &cfg.service));
759 match Listener::bind(&endpoint, options.max_pipe_instances) {
760 Ok(listener) => break (listener, election.unwrap()),
761 Err(e) if e.kind() == std::io::ErrorKind::AddrInUse => {}
764 Err(e) => return Err(HostError::Io(e)),
765 }
766 }
767 _ => {}
770 }
771 drop(election);
772 let remaining = deadline.saturating_duration_since(Instant::now());
773 if remaining.is_zero() {
774 clear_takeover_if_owner(suite, &cfg.service, me);
775 return Err(match unreadable {
778 Some(e) => HostError::Io(e),
779 None => HostError::TakeoverTimeout,
780 });
781 }
782 std::thread::sleep(backoff.min(remaining));
783 backoff = (backoff * 2).min(Duration::from_millis(50));
784 };
785 let record = ServiceRecord {
786 protocol_major: PROTOCOL_MAJOR,
787 protocol_minor: PROTOCOL_MINOR,
788 service: cfg.service.clone(),
789 role: cfg.role.clone(),
790 rank: cfg.rank,
791 pid: me,
792 endpoint: endpoint.clone(),
793 version: cfg.version.clone(),
794 engine_version: cfg.version.clone(),
795 started_ms: now_ms(),
796 process_started_ms: crate::discovery::own_start_ms().unwrap_or(0),
797 };
798 if let Err(e) = write_atomic(
799 &record_path(suite, &cfg.service),
800 &serde_json::to_vec_pretty(&record).unwrap_or_default(),
801 ) {
802 listener.cleanup();
803 return Err(HostError::Io(e));
804 }
805 if read_takeover(suite, &cfg.service).is_some_and(|t| t.pid == me) {
806 let _ = std::fs::remove_file(takeover_path(suite, &cfg.service));
807 }
808 let shared = Arc::new(Shared {
809 suite: suite.clone(),
810 cfg,
811 handler,
812 listener,
813 election: Mutex::new(Some(election)),
814 record,
815 stop: AtomicBool::new(false),
816 handoff: Arc::new(AtomicBool::new(false)),
817 released: Mutex::new(false),
818 released_cv: Condvar::new(),
819 inflight: AtomicUsize::new(0),
820 next_conn: AtomicU64::new(1),
821 conns: Mutex::new(HashMap::new()),
822 bell: Arc::new(Bell::new()),
823 watch,
824 write_timeout: options.write_timeout,
825 max_message: options.max_message,
826 max_connections: options.max_connections,
827 live_conns: Arc::new(AtomicUsize::new(0)),
828 reserved_slots: options.reserved_slots,
829 supervisor: options.get_supervisor_pid(),
830 reserved_conns: Arc::new(AtomicUsize::new(0)),
831 pre_handshake: Arc::new(AtomicUsize::new(0)),
832 handshake_timeout: options.handshake_timeout,
833 fails: Mutex::new(crate::fail_limit::FailLimiter::new()),
834 });
835 takeover_watch::register(
836 takeover_path(&shared.suite, &shared.cfg.service),
837 &shared.bell,
838 );
839 shared.event(ServiceEvent::Started {
840 service: shared.cfg.service.clone(),
841 endpoint,
842 rank: shared.cfg.rank,
843 });
844 let s = shared.clone();
845 if let Err(e) = std::thread::Builder::new()
846 .name("rk-svc-accept".into())
847 .spawn(move || accept_loop(s))
848 {
849 shared.release();
850 return Err(HostError::Io(e));
851 }
852 let s = shared.clone();
853 if let Err(e) = std::thread::Builder::new()
854 .name("rk-svc-takeover".into())
855 .spawn(move || takeover_monitor(s))
856 {
857 shared.release();
858 return Err(HostError::Io(e));
859 }
860 Ok(Hosted { shared })
861}
862
863pub(crate) fn now_ms() -> u64 {
864 std::time::SystemTime::now()
865 .duration_since(std::time::UNIX_EPOCH)
866 .map(|d| d.as_millis() as u64)
867 .unwrap_or(0)
868}
869
870fn reserved_slot(s: &Shared, peer: &crate::PeerIdentity) -> Option<ConnSlot> {
874 if !matches!((peer.pid, s.supervisor), (Some(pid), Some(owner)) if pid == owner) {
875 return None;
876 }
877 let slot = ConnSlot(s.reserved_conns.clone());
878 let used = s.reserved_conns.fetch_add(1, Ordering::SeqCst) + 1;
879 (used <= s.reserved_slots).then_some(slot)
880}
881
882fn accept_loop(s: Arc<Shared>) {
883 let mut full_reported = false;
886 while !s.stop.load(Ordering::SeqCst) {
887 match s.listener.accept() {
888 Ok(Accepted::Conn(conn)) => {
889 if s.stop.load(Ordering::SeqCst) {
890 break;
891 }
892 let table = ConnSlot(s.live_conns.clone());
895 let live = s.live_conns.fetch_add(1, Ordering::SeqCst) + 1;
896 let slot = if !cfg!(unix) || live <= s.max_connections {
897 full_reported = false;
899 table
900 } else {
901 drop(table);
902 match reserved_slot(&s, &conn.peer) {
903 Some(slot) => slot,
904 None => {
905 (conn.shutdown)();
906 s.event(ServiceEvent::PeerRejected {
907 reason: format!(
908 "connection limit of {} reached",
909 s.max_connections
910 ),
911 });
912 if !std::mem::replace(&mut full_reported, true) {
913 s.event(ServiceEvent::TableFull {
914 limit: s.max_connections,
915 reserved: s.reserved_slots,
916 pre_handshake: s.pre_handshake.load(Ordering::SeqCst),
917 attached: s
918 .conns
919 .lock()
920 .unwrap_or_else(|e| e.into_inner())
921 .len(),
922 peer_pid: conn.peer.pid,
923 });
924 }
925 continue;
926 }
927 }
928 };
929 let pre = PreHandshake::enter(&s.pre_handshake);
930 let s2 = s.clone();
931 let _ = std::thread::Builder::new()
932 .name("rk-svc-conn".into())
933 .spawn(move || {
934 let _slot = slot;
935 serve_conn(s2, conn, pre)
936 });
937 }
938 Ok(Accepted::Rejected(reason)) => {
941 if !s.stop.load(Ordering::SeqCst) {
942 s.event(ServiceEvent::PeerRejected { reason });
943 }
944 }
945 Ok(Accepted::Vanished) => {}
946 Err(_) => {
947 if s.stop.load(Ordering::SeqCst) {
948 break;
949 }
950 std::thread::sleep(Duration::from_millis(20));
951 }
952 }
953 }
954}
955
956type FileStamp = (Option<SystemTime>, u64);
958
959fn file_stamp(path: &std::path::Path) -> Option<FileStamp> {
960 let md = std::fs::metadata(path).ok()?;
961 Some((md.modified().ok(), md.len()))
962}
963
964fn obliging_rank(
967 cfg: &ServiceConfig,
968 own_pid: u32,
969 alive: fn(u32) -> bool,
970 t: &TakeoverRequest,
971 now: u64,
972) -> Option<u8> {
973 let max_age = cfg.takeover_timeout.as_millis() as u64;
974 let fresh = now.saturating_sub(t.requested_at_ms) <= max_age + 5_000;
975 let their = resolve_rank(cfg, &t.role, t.rank);
976 let obliges =
977 their != RANK_UNKNOWN && their < cfg.rank && t.pid != own_pid && alive(t.pid) && fresh;
978 obliges.then_some(their)
979}
980
981#[cfg(test)]
982struct ExitMark<'a>(&'a Bell);
983
984#[cfg(test)]
985impl Drop for ExitMark<'_> {
986 fn drop(&mut self) {
987 self.0.mark_exited();
988 }
989}
990
991fn takeover_monitor(s: Arc<Shared>) {
1001 #[cfg(test)]
1002 let _exit = ExitMark(&s.bell);
1003 let path = takeover_path(&s.suite, &s.cfg.service);
1004 let mut judged: Option<FileStamp> = None;
1005 let mut look_now = false;
1006 loop {
1007 if s.stop.load(Ordering::SeqCst) {
1008 return;
1009 }
1010 let force = std::mem::take(&mut look_now);
1011 match file_stamp(&path) {
1012 None => judged = None,
1013 Some(stamp) if force || judged != Some(stamp) => {
1014 judged = Some(stamp);
1015 #[cfg(test)]
1016 s.bell.note_read();
1017 match try_read_takeover(&s.suite, &s.cfg.service) {
1018 Ok(Some(t)) => {
1019 let now = now_ms();
1020 let obliging = obliging_rank(&s.cfg, s.record.pid, s.watch.alive, &t, now);
1021 if let Some(their) = obliging {
1022 hand_off(&s, t, their);
1023 return;
1024 }
1025 }
1026 Ok(None) => {}
1027 Err(_) => judged = None,
1031 }
1032 }
1033 Some(_) => {}
1034 }
1035 #[cfg(test)]
1036 s.bell.note_pass();
1037 match s.bell.wait(s.watch.interval) {
1038 Wake::Stop => return,
1039 Wake::Poke => look_now = true,
1040 Wake::Timeout => {}
1041 }
1042 }
1043}
1044
1045fn hand_off(s: &Shared, t: TakeoverRequest, their: u8) {
1048 s.handoff.store(true, Ordering::SeqCst);
1049 s.event(ServiceEvent::HandoffRequested {
1050 by_role: t.role,
1051 by_rank: their,
1052 });
1053 s.handler.on_handoff();
1054 let until = Instant::now() + s.cfg.handoff_grace;
1055 while s.inflight.load(Ordering::SeqCst) > 0
1056 && Instant::now() < until
1057 && !s.stop.load(Ordering::SeqCst)
1058 {
1059 std::thread::sleep(Duration::from_millis(10));
1060 }
1061 s.release();
1062}
1063
1064fn frame_err(e: &FrameError) -> ServiceError {
1065 ServiceError::new("bad_frame", e.to_string())
1066}
1067
1068fn serve_conn(s: Arc<Shared>, conn: Conn, mut pre: PreHandshake) {
1069 let Conn {
1070 reader,
1071 writer,
1072 peer_closed,
1073 shutdown,
1074 peer,
1075 } = conn;
1076 let mut reader = BufReader::new(reader);
1077 let writer = Arc::new(Mutex::new(writer));
1078 let limit = s.cfg.frame_limit;
1079 let bound = s
1080 .write_timeout
1081 .map(|d| (d, DeadlineWatch::start(shutdown.clone())));
1082 let _watcher_ends = bound.as_ref().map(|(_, b)| FinishOnDrop(b.clone()));
1084 let write = {
1088 let w = writer.clone();
1089 let bound = bound.clone();
1090 move |v: &Value| -> bool {
1091 let mut w = w.lock().unwrap_or_else(|e| e.into_inner());
1092 if let Some((d, b)) = &bound {
1093 b.set(Some(Instant::now() + *d));
1094 }
1095 let r = match write_json_line(&mut *w, v, limit) {
1096 Ok(()) => {
1097 let _ = w.flush();
1098 true
1099 }
1100 Err(FrameError::TooLarge { .. }) => false,
1101 Err(_) => true,
1102 };
1103 if let Some((_, b)) = &bound {
1104 b.set(None);
1105 }
1106 r
1107 }
1108 };
1109 let send = {
1110 let write = write.clone();
1111 move |v: &Value| {
1112 write(v);
1113 }
1114 };
1115 let mut buf = Vec::new();
1116 let conn_key = s.next_conn.fetch_add(1, Ordering::SeqCst);
1118 let _forget_fails = ForgetFails {
1119 s: s.clone(),
1120 conn: conn_key,
1121 };
1122 let handshake = s.handshake_timeout.map(|d| {
1126 let (s, shutdown) = (s.clone(), shutdown.clone());
1127 let watch = DeadlineWatch::start(Arc::new(move || {
1128 s.event(ServiceEvent::ConnectionRejected {
1129 app: String::new(),
1130 reason: format!("handshake deadline {} ms passed", d.as_millis()),
1131 });
1132 shutdown();
1133 }));
1134 watch.set(Some(Instant::now() + d));
1135 watch
1136 });
1137 let _handshake_ends = handshake.as_ref().map(|w| FinishOnDrop(w.clone()));
1138 let hello: Value = match read_json_line(&mut reader, &mut buf, 64 * 1024) {
1140 Ok(Some(v)) => v,
1141 _ => return, };
1143 let app = hello["app"].as_str().unwrap_or("").to_string();
1144 let reject = |reason: &str, code: &str, retryable: bool| {
1145 let e = ServiceError {
1146 code: code.into(),
1147 message: reason.into(),
1148 retryable,
1149 details: None,
1150 };
1151 send(&json!({"type": "hello", "ok": false, "error": e.to_value()}));
1152 s.event(ServiceEvent::ConnectionRejected {
1153 app: app.clone(),
1154 reason: reason.into(),
1155 });
1156 shutdown();
1157 };
1158 if hello["type"] != "hello" || app.is_empty() {
1159 return reject(
1160 "first frame must be hello with an app label",
1161 "bad_hello",
1162 false,
1163 );
1164 }
1165 if let Some(secret) = &s.cfg.private_secret {
1166 if !secret.verify(hello["secret"].as_str().unwrap_or("")) {
1169 let now = Instant::now();
1170 let limited = s
1171 .fails
1172 .lock()
1173 .unwrap_or_else(|e| e.into_inner())
1174 .refuses_unverified(conn_key, now);
1175 if limited {
1176 return reject(
1177 "too many failed authentications; wait before reconnecting",
1178 crate::error::ERROR_RATE_LIMITED,
1179 false,
1180 );
1181 }
1182 s.fails
1183 .lock()
1184 .unwrap_or_else(|e| e.into_inner())
1185 .record_failure(conn_key, now);
1186 return reject(
1187 "missing or wrong secret",
1188 crate::error::ERROR_UNAUTHENTICATED,
1189 false,
1190 );
1191 }
1192 }
1193 if hello["protocol_major"].as_u64() != Some(u64::from(PROTOCOL_MAJOR)) {
1194 return reject("protocol major mismatch", "protocol_mismatch", false);
1195 }
1196 if s.handoff.load(Ordering::SeqCst) {
1197 return reject(
1198 "host is handing off",
1199 crate::error::ERROR_HOST_HANDOFF,
1200 true,
1201 );
1202 }
1203 if !s.cfg.allowlist.app_known(&app) {
1204 return reject(
1205 "app is not on the service allowlist",
1206 crate::error::ERROR_NOT_ALLOWED,
1207 false,
1208 );
1209 }
1210 let id = conn_key;
1211 s.conns
1212 .lock()
1213 .unwrap_or_else(|e| e.into_inner())
1214 .insert(id, shutdown.clone());
1215 pre.settle();
1218 if let Some(watch) = &handshake {
1219 watch.finish();
1220 }
1221 s.event(ServiceEvent::ConnectionOpened {
1222 conn: id,
1223 app: app.clone(),
1224 });
1225 send(&json!({
1226 "type": "hello", "ok": true, "service": s.cfg.service, "role": s.cfg.role, "rank": s.cfg.rank,
1227 "version": s.cfg.version, "protocol_major": PROTOCOL_MAJOR, "protocol_minor": PROTOCOL_MINOR,
1228 "methods": s.cfg.allowlist.methods_for(&app),
1229 "frame_limit": limit, "max_message": s.max_message,
1230 "chunk_bytes": crate::chunk::chunk_bytes_for(limit),
1231 }));
1232
1233 let caller = Caller::local_app(app.clone(), peer.clone());
1234 let closed_notified = Arc::new(AtomicBool::new(false));
1235 let notify_closed = {
1236 let (s, flag) = (s.clone(), closed_notified.clone());
1237 move || {
1238 if !flag.swap(true, Ordering::SeqCst) {
1239 s.handler.connection_closed(id);
1240 }
1241 }
1242 };
1243 let chunk_bytes = crate::chunk::chunk_bytes_for(limit);
1246 let mut uploads = crate::chunk::Uploads::default();
1247 let mut download: Option<crate::chunk::Download> = None;
1248 let mut downloads_made: u64 = 0;
1249 loop {
1251 let req: Value = match read_json_line(&mut reader, &mut buf, limit) {
1252 Ok(Some(v)) => v,
1253 Ok(None) | Err(FrameError::Io(_)) => break,
1254 Err(e) => {
1255 send(
1256 &json!({"type": "response", "id": 0, "ok": false, "error": frame_err(&e).to_value()}),
1257 );
1258 continue;
1259 }
1260 };
1261 if let Some(secret) = &s.cfg.private_secret {
1262 if !secret.verify(req["secret"].as_str().unwrap_or("")) {
1265 let now = Instant::now();
1266 let limited = s
1267 .fails
1268 .lock()
1269 .unwrap_or_else(|e| e.into_inner())
1270 .refuses_unverified(conn_key, now);
1271 if limited {
1272 s.event(ServiceEvent::ConnectionRejected {
1273 app: app.clone(),
1274 reason: "too many failed authentications".into(),
1275 });
1276 send(
1277 &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1278 "error": ServiceError::new(crate::error::ERROR_RATE_LIMITED, "too many failed authentications; wait before reconnecting").to_value()}),
1279 );
1280 break;
1281 }
1282 s.fails
1283 .lock()
1284 .unwrap_or_else(|e| e.into_inner())
1285 .record_failure(conn_key, now);
1286 s.event(ServiceEvent::ConnectionRejected {
1287 app: app.clone(),
1288 reason: "missing or wrong secret".into(),
1289 });
1290 send(
1291 &json!({"type": "response", "id": req["id"].as_u64().unwrap_or(0), "ok": false,
1292 "error": ServiceError::new(crate::error::ERROR_UNAUTHENTICATED, "missing or wrong secret").to_value()}),
1293 );
1294 break;
1295 }
1296 }
1297 let Some(rid) = req["id"].as_u64().filter(|_| req["type"] == "request") else {
1298 send(
1299 &json!({"type": "response", "id": 0, "ok": false, "error": ServiceError::new("bad_request", "expected {type:request,id,method}").to_value()}),
1300 );
1301 continue;
1302 };
1303 let respond_err = |e: ServiceError| {
1304 send(&json!({"type": "response", "id": rid, "ok": false, "error": e.to_value()}))
1305 };
1306 uploads.expire();
1308 let mut method = req["method"].as_str().unwrap_or("").to_string();
1309 let chunked_ok = req["chunked_ok"] == true && s.max_message > 0;
1310 if s.handoff.load(Ordering::SeqCst) {
1311 respond_err(ServiceError::handoff());
1312 continue;
1313 }
1314 let mut assembled: Option<Value> = None;
1317 if method == crate::chunk::METHOD_FETCH {
1318 let answer = match &download {
1319 Some(d) => d.fetch(req.get("params").unwrap_or(&Value::Null)),
1320 None => Err(ServiceError::new(
1321 crate::error::ERROR_BAD_CHUNK,
1322 "no chunked result is held on this connection",
1323 )),
1324 };
1325 match answer {
1326 Ok(v) => send(&json!({"type": "response", "id": rid, "ok": true, "result": v})),
1327 Err(e) => respond_err(e),
1328 }
1329 continue;
1330 }
1331 if method == crate::chunk::METHOD_UPLOAD {
1332 if s.max_message == 0 {
1333 respond_err(ServiceError::new(
1334 crate::error::ERROR_REQUEST_TOO_LARGE,
1335 "this host does not accept chunked requests",
1336 ));
1337 continue;
1338 }
1339 let authorize = |m: &str| {
1342 crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, m).inspect_err(
1343 |_| {
1344 s.event(ServiceEvent::Denied {
1345 conn: id,
1346 app: app.clone(),
1347 method: m.to_string(),
1348 });
1349 },
1350 )
1351 };
1352 match uploads.accept_with(
1353 req.get("params").unwrap_or(&Value::Null),
1354 s.max_message,
1355 &authorize,
1356 ) {
1357 Ok(crate::chunk::UploadStep::More { received }) => {
1358 send(&json!({"type": "response", "id": rid, "ok": true,
1359 "result": {"received": received}}));
1360 continue;
1361 }
1362 Ok(crate::chunk::UploadStep::Done { method: m, params }) => {
1363 method = m;
1364 assembled = Some(params);
1365 }
1366 Err(e) => {
1367 respond_err(e);
1368 continue;
1369 }
1370 }
1371 }
1372 if let Err(e) = crate::contract::authorize_allowlist(&s.cfg.allowlist, &caller, &method) {
1373 s.event(ServiceEvent::Denied {
1374 conn: id,
1375 app: app.clone(),
1376 method: method.clone(),
1377 });
1378 respond_err(e);
1379 continue;
1380 }
1381 if method == "ping" {
1382 send(
1383 &json!({"type": "response", "id": rid, "ok": true, "result": {"pong": true, "rank": s.cfg.rank}}),
1384 );
1385 continue;
1386 }
1387 let params =
1388 assembled.unwrap_or_else(|| req.get("params").cloned().unwrap_or_else(|| json!({})));
1389 let cx = new_context(
1390 id,
1391 caller.clone(),
1392 Arc::new(AtomicBool::new(false)),
1393 s.handoff.clone(),
1394 );
1395 let emit: Emit = {
1396 let (write, s, method) = (write.clone(), s.clone(), method.clone());
1397 Arc::new(move |ev| {
1398 if !write(&json!({"type": "event", "id": rid, "event": ev})) {
1399 s.event(ServiceEvent::FrameTooLarge {
1400 conn: id,
1401 method: method.clone(),
1402 kind: "event",
1403 limit,
1404 });
1405 }
1406 })
1407 };
1408 s.inflight.fetch_add(1, Ordering::SeqCst);
1409 let done = AtomicBool::new(false);
1410 let result = std::thread::scope(|sc| {
1411 let watcher = sc.spawn(|| {
1414 while !done.load(Ordering::SeqCst) {
1415 if peer_closed() {
1416 cx.closed.store(true, Ordering::SeqCst);
1417 notify_closed();
1418 return;
1419 }
1420 std::thread::park_timeout(Duration::from_millis(40));
1421 }
1422 });
1423 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1424 s.handler.call(&cx, &method, ¶ms, &emit)
1425 }))
1426 .unwrap_or_else(|_| Err(ServiceError::new("panic", "handler panicked")));
1427 done.store(true, Ordering::SeqCst);
1428 watcher.thread().unpark();
1429 r
1430 });
1431 s.inflight.fetch_sub(1, Ordering::SeqCst);
1432 if cx.peer_closed() {
1433 break;
1434 }
1435 match result {
1436 Ok(v) => {
1437 if !write(&json!({"type": "response", "id": rid, "ok": true, "result": v})) {
1440 s.event(ServiceEvent::FrameTooLarge {
1441 conn: id,
1442 method: method.clone(),
1443 kind: "response",
1444 limit,
1445 });
1446 let held = chunked_ok
1449 .then(|| serde_json::to_string(&v).ok())
1450 .flatten()
1451 .filter(|text| text.len() <= s.max_message);
1452 drop(v);
1453 match held {
1454 Some(text) => {
1455 downloads_made += 1;
1456 let held = crate::chunk::Download::new(
1457 format!("d{downloads_made}"),
1458 text,
1459 chunk_bytes,
1460 );
1461 let descriptor = held.descriptor();
1462 download = Some(held);
1463 send(&json!({"type": "response", "id": rid, "ok": true,
1464 "result": Value::Null, "chunked": descriptor}));
1465 }
1466 None => respond_err(ServiceError::new(
1467 ERROR_RESPONSE_TOO_LARGE,
1468 format!(
1469 "the result of {method} is larger than the {limit}-byte frame limit"
1470 ),
1471 )),
1472 }
1473 }
1474 }
1475 Err(e) => respond_err(e),
1476 }
1477 }
1478 notify_closed();
1479 s.conns
1480 .lock()
1481 .unwrap_or_else(|e| e.into_inner())
1482 .remove(&id);
1483 shutdown();
1484}
1485
1486#[allow(dead_code)]
1487fn _assert_transport_used() {
1488 let _ = transport::current_uid;
1489}
1490
1491#[cfg(test)]
1492mod options_tests {
1493 use super::*;
1494
1495 #[test]
1496 fn pipe_instance_cap_is_small_finite_and_clamped() {
1497 let d = HostOptions::default();
1498 assert_eq!(d.get_max_pipe_instances(), DEFAULT_MAX_PIPE_INSTANCES);
1499 assert!(d.get_max_pipe_instances() >= 1 && d.get_max_pipe_instances() < 255);
1500 assert_eq!(
1501 HostOptions::new()
1502 .max_pipe_instances(4)
1503 .get_max_pipe_instances(),
1504 4
1505 );
1506 assert_eq!(
1507 HostOptions::new()
1508 .max_pipe_instances(0)
1509 .get_max_pipe_instances(),
1510 1
1511 );
1512 assert_eq!(
1514 HostOptions::new()
1515 .max_pipe_instances(u32::MAX)
1516 .get_max_pipe_instances(),
1517 254
1518 );
1519 }
1520
1521 #[test]
1522 fn handshake_deadline_can_be_removed_and_never_reaches_zero() {
1523 assert_eq!(
1524 HostOptions::new()
1525 .handshake_timeout(None)
1526 .get_handshake_timeout(),
1527 None
1528 );
1529 assert_eq!(
1530 HostOptions::new()
1531 .handshake_timeout(Some(Duration::ZERO))
1532 .get_handshake_timeout(),
1533 Some(Duration::from_millis(1))
1534 );
1535 }
1536
1537 #[cfg(unix)]
1538 #[test]
1539 fn the_supervisor_is_the_parent_unless_named_or_removed() {
1540 assert_eq!(
1541 HostOptions::default().get_supervisor_pid(),
1542 Some(std::os::unix::process::parent_id())
1543 );
1544 assert_eq!(
1545 HostOptions::new()
1546 .supervisor_pid(Some(7))
1547 .get_supervisor_pid(),
1548 Some(7)
1549 );
1550 assert_eq!(
1551 HostOptions::new().supervisor_pid(None).get_supervisor_pid(),
1552 None
1553 );
1554 }
1555
1556 #[test]
1557 fn a_connection_leaves_the_pre_handshake_count_exactly_once() {
1558 let count = Arc::new(AtomicUsize::new(0));
1559 let mut a = PreHandshake::enter(&count);
1560 let b = PreHandshake::enter(&count);
1561 assert_eq!(count.load(Ordering::SeqCst), 2);
1562 a.settle();
1564 a.settle();
1565 assert_eq!(count.load(Ordering::SeqCst), 1);
1566 drop(a);
1567 assert_eq!(count.load(Ordering::SeqCst), 1);
1568 drop(b);
1570 assert_eq!(count.load(Ordering::SeqCst), 0);
1571 }
1572}
1573
1574#[cfg(test)]
1575mod monitor_tests {
1576 use super::*;
1577 use std::sync::atomic::AtomicUsize;
1578
1579 const LONG: Duration = Duration::from_secs(600);
1581
1582 struct Quiet {
1583 handoffs: AtomicUsize,
1584 }
1585
1586 impl Handler for Quiet {
1587 fn call(
1588 &self,
1589 _cx: &CallContext,
1590 _method: &str,
1591 _params: &Value,
1592 _emit: &Emit,
1593 ) -> Result<Value, ServiceError> {
1594 Ok(json!({}))
1595 }
1596 fn on_handoff(&self) {
1597 self.handoffs.fetch_add(1, Ordering::SeqCst);
1598 }
1599 }
1600
1601 fn wait_for(what: &str, mut ok: impl FnMut() -> bool) {
1602 let deadline = Instant::now() + Duration::from_secs(20);
1603 while !ok() {
1604 assert!(Instant::now() < deadline, "timed out waiting for {what}");
1605 std::thread::sleep(Duration::from_millis(5));
1606 }
1607 }
1608
1609 fn host_in(
1611 dir: &tempfile::TempDir,
1612 interval: Duration,
1613 alive: fn(u32) -> bool,
1614 ) -> (Suite, Hosted, Arc<Quiet>) {
1615 let suite = Suite::at(dir.path().join("suite")).unwrap();
1616 let mut cfg = ServiceConfig::new(
1617 "watch",
1618 "incumbent",
1619 5,
1620 "test",
1621 Allowlist::new().allow("*", ["echo"]),
1622 );
1623 cfg.takeover_timeout = Duration::from_secs(20);
1624 cfg.handoff_grace = Duration::from_secs(1);
1625 let handler = Arc::new(Quiet {
1626 handoffs: AtomicUsize::new(0),
1627 });
1628 let hosted = start_host(
1629 &suite,
1630 cfg,
1631 handler.clone(),
1632 HostOptions::default(),
1633 WatchTuning { interval, alive },
1634 )
1635 .expect("host starts");
1636 (suite, hosted, handler)
1637 }
1638
1639 fn request(role: &str, rank: u8, pid: u32) -> TakeoverRequest {
1640 TakeoverRequest {
1641 role: role.into(),
1642 rank,
1643 pid,
1644 requested_at_ms: now_ms(),
1645 }
1646 }
1647
1648 fn other_pid() -> u32 {
1650 std::process::id().wrapping_add(1)
1651 }
1652
1653 #[test]
1654 fn a_request_from_another_process_is_found_by_the_file_check() {
1655 let dir = tempfile::tempdir().unwrap();
1656 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(50), |_| true);
1657 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1659 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1660 assert!(
1661 hosted.wait_released(Duration::from_secs(20)),
1662 "the incumbent must hand off"
1663 );
1664 assert!(hosted.handoff_requested());
1665 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1666 }
1667
1668 #[test]
1669 fn a_request_written_in_this_process_wakes_the_monitor_without_the_interval() {
1670 let dir = tempfile::tempdir().unwrap();
1671 let (suite, hosted, handler) = host_in(&dir, LONG, |_| true);
1672 let bell = hosted.shared.bell.clone();
1673 wait_for("the first pass", || bell.passes() >= 1);
1674 write_takeover(&suite, "watch", &request("shell", 0, other_pid())).unwrap();
1675 assert!(
1676 hosted.wait_released(Duration::from_secs(20)),
1677 "a 10-minute interval must not delay a request raised in this process"
1678 );
1679 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1680 }
1681
1682 #[test]
1683 fn releasing_the_host_ends_the_monitor_at_once() {
1684 let dir = tempfile::tempdir().unwrap();
1685 let (_suite, hosted, _handler) = host_in(&dir, LONG, pid_alive);
1686 let bell = hosted.shared.bell.clone();
1687 wait_for("the first pass", || bell.passes() >= 1);
1688 assert!(!bell.exited());
1689 hosted.shutdown();
1690 wait_for("the monitor thread to return", || bell.exited());
1691 }
1692
1693 #[test]
1694 fn an_unchanged_request_that_does_not_oblige_is_stat_ed_not_read_again() {
1695 let dir = tempfile::tempdir().unwrap();
1696 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1697 let bell = hosted.shared.bell.clone();
1698 let body = serde_json::to_vec(&request("peer", 5, other_pid())).unwrap();
1700 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1701 wait_for("the request to be judged", || bell.reads() >= 1);
1702 let before = bell.wakes();
1703 wait_for("five more checks", || bell.wakes() >= before + 5);
1704 assert_eq!(bell.reads(), 1, "an unchanged file must not be read again");
1705 assert!(!hosted.handoff_requested());
1706
1707 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1709 write_atomic(&takeover_path(&suite, "watch"), &body).unwrap();
1710 assert!(hosted.wait_released(Duration::from_secs(20)));
1711 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1712 }
1713
1714 #[test]
1715 fn a_quiet_host_wakes_its_monitor_about_once_a_second() {
1716 let dir = tempfile::tempdir().unwrap();
1717 let (_suite, hosted, _handler) = host_in(&dir, TAKEOVER_CHECK_INTERVAL, pid_alive);
1718 let bell = hosted.shared.bell.clone();
1719 let (start, woke_before) = (Instant::now(), bell.wakes());
1720 std::thread::sleep(Duration::from_secs(2));
1721 let (secs, woke) = (start.elapsed().as_secs_f64(), bell.wakes() - woke_before);
1722 let allowed = 3u64.max(secs.ceil() as u64 + 1);
1725 assert!(
1726 woke <= allowed,
1727 "the monitor woke {woke} times in {secs:.1} s"
1728 );
1729 drop(hosted);
1730 }
1731
1732 #[test]
1735 fn an_unreadable_request_is_looked_at_again_not_judged_absent() {
1736 let dir = tempfile::tempdir().unwrap();
1737 let (suite, hosted, handler) = host_in(&dir, Duration::from_millis(20), |_| true);
1738 let bell = hosted.shared.bell.clone();
1739 let path = takeover_path(&suite, "watch");
1740 std::fs::create_dir_all(&path).unwrap();
1741 wait_for("three reads of the unreadable file", || bell.reads() >= 3);
1743 assert!(!hosted.handoff_requested());
1744
1745 std::fs::remove_dir(&path).unwrap();
1747 let body = serde_json::to_vec(&request("shell", 0, other_pid())).unwrap();
1748 write_atomic(&path, &body).unwrap();
1749 assert!(hosted.wait_released(Duration::from_secs(20)));
1750 assert_eq!(handler.handoffs.load(Ordering::SeqCst), 1);
1751 }
1752
1753 #[test]
1754 fn an_unreadable_record_blocks_the_election_instead_of_being_replaced() {
1755 let dir = tempfile::tempdir().unwrap();
1756 let suite = Suite::at(dir.path().join("suite")).unwrap();
1757 let record = record_path(&suite, "watch");
1758 std::fs::create_dir_all(&record).unwrap();
1759 let mut cfg = ServiceConfig::new("watch", "newcomer", 5, "test", Allowlist::new());
1760 cfg.takeover_timeout = Duration::from_millis(300);
1761 let err = start_host(
1762 &suite,
1763 cfg,
1764 Arc::new(Quiet {
1765 handoffs: AtomicUsize::new(0),
1766 }),
1767 HostOptions::default(),
1768 WatchTuning {
1769 interval: LONG,
1770 alive: |_| true,
1771 },
1772 )
1773 .err()
1774 .expect("a record that cannot be read must not be treated as no record");
1775 match err {
1776 HostError::Io(e) => assert_ne!(e.kind(), std::io::ErrorKind::NotFound),
1777 _ => panic!("expected the read error"),
1778 }
1779 assert!(record.is_dir(), "left as it was");
1780 }
1781
1782 #[test]
1783 fn only_a_fresh_live_better_ranked_request_from_another_process_obliges() {
1784 let mut cfg = ServiceConfig::new("watch", "incumbent", 5, "test", Allowlist::new());
1785 cfg.takeover_timeout = Duration::from_secs(10);
1786 cfg.known_roles.insert("legacy".into(), 1);
1787 let now = 1_000_000_000_000u64;
1788 let live: fn(u32) -> bool = |_| true;
1789 let dead: fn(u32) -> bool = |_| false;
1790 let at = |role: &str, rank: u8, pid: u32, requested_at_ms: u64| TakeoverRequest {
1791 role: role.into(),
1792 rank,
1793 pid,
1794 requested_at_ms,
1795 };
1796
1797 assert_eq!(
1798 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now), now),
1799 Some(0)
1800 );
1801 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 5, 9, now), now), None);
1803 assert_eq!(obliging_rank(&cfg, 7, live, &at("x", 6, 9, now), now), None);
1804 assert_eq!(
1806 obliging_rank(&cfg, 9, live, &at("shell", 0, 9, now), now),
1807 None
1808 );
1809 assert_eq!(
1811 obliging_rank(&cfg, 7, dead, &at("shell", 0, 9, now), now),
1812 None
1813 );
1814 assert_eq!(
1816 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_000), now),
1817 Some(0)
1818 );
1819 assert_eq!(
1820 obliging_rank(&cfg, 7, live, &at("shell", 0, 9, now - 15_001), now),
1821 None
1822 );
1823 assert_eq!(
1825 obliging_rank(&cfg, 7, live, &at("legacy", RANK_UNKNOWN, 9, now), now),
1826 Some(1)
1827 );
1828 assert_eq!(
1829 obliging_rank(&cfg, 7, live, &at("stranger", RANK_UNKNOWN, 9, now), now),
1830 None
1831 );
1832 }
1833}