kunobi_daemon/control/
watch.rs1use super::ControlService;
3use crate::wire::{
4 self, Control, Hello, LifecycleChange, LifecycleEvent, MessageKind, capability, operation,
5};
6use buffa::Message;
7use std::{io, time::Duration};
8use tokio::{
9 io::{AsyncRead, AsyncWrite},
10 sync::watch,
11 time::Instant,
12};
13
14#[derive(Clone, PartialEq)]
15struct Published {
16 generation: Option<u64>,
17 build: String,
18 retiring: bool,
19}
20
21pub(super) struct Publisher(watch::Sender<Published>);
22impl Publisher {
23 pub(super) fn new() -> Self {
24 Self(
25 watch::channel(Published {
26 generation: None,
27 build: String::new(),
28 retiring: false,
29 })
30 .0,
31 )
32 }
33 pub(super) fn notify(&self) {
34 self.0.send_modify(|_| {});
35 }
36}
37
38fn invalid(message: &'static str) -> io::Error {
39 io::Error::new(io::ErrorKind::InvalidData, message)
40}
41
42impl ControlService {
43 pub fn selection_committed(&self, generation: u64, build: String) -> io::Result<()> {
49 if generation == self.generation && build != self.build {
50 return Err(invalid("selection does not match serving build"));
51 }
52 let mut result = Ok(());
53 self.events.0.send_if_modified(|state| {
54 if state
55 .generation
56 .is_some_and(|previous| generation < previous)
57 || (Some(generation) == state.generation && build != state.build)
58 {
59 result = Err(invalid("selection must advance monotonically"));
60 return false;
61 }
62 if Some(generation) == state.generation {
63 return false;
64 }
65 state.generation = Some(generation);
66 state.build = build;
67 true
68 });
69 result
70 }
71
72 pub fn mark_retiring(&self) {
75 self.lifecycle.start_drain();
76 self.events.0.send_if_modified(|state| {
77 if state.retiring {
78 return false;
79 }
80 state.retiring = true;
81 true
82 });
83 }
84
85 pub(super) fn validate_watch(&self, request: &Control) -> io::Result<()> {
86 if !request.payload.is_empty()
87 || !request.token.is_empty()
88 || request.offset.is_some()
89 || (request.generation != 0 && request.generation != self.generation)
90 {
91 return Err(invalid("invalid WATCH request"));
92 }
93 Ok(())
94 }
95
96 pub(super) async fn serve_watch<S: AsyncRead + AsyncWrite + Unpin>(
97 &self,
98 mut session: wire::AsyncSession<S>,
99 request: Control,
100 ) -> io::Result<()> {
101 let mut changes = self.events.0.subscribe();
104 let mut previous: Option<LifecycleEvent> = None;
105 let mut sequence = 0u64;
106 loop {
107 let published = changes.borrow_and_update().clone();
108 let draining = self.lifecycle.snapshot().draining;
109 let mut event = LifecycleEvent {
110 process_id: self.process_id,
111 generation: self.generation,
112 build: self.build.clone(),
113 ready: self.ready.load(std::sync::atomic::Ordering::Acquire) && !draining,
114 draining,
115 selected_generation: published.generation,
116 selected_build: published.build,
117 retiring: published.retiring,
118 ..Default::default()
119 };
120 if let Some(old) = &previous {
121 let changed = [
122 (
123 event.ready != old.ready && event.ready,
124 LifecycleChange::Ready,
125 ),
126 (event.draining != old.draining, LifecycleChange::Draining),
127 (
128 event.selected_generation != old.selected_generation,
129 LifecycleChange::SelectionChanged,
130 ),
131 (event.retiring != old.retiring, LifecycleChange::Retiring),
132 ];
133 let mut kinds = changed
134 .into_iter()
135 .filter_map(|(changed, kind)| changed.then_some(kind));
136 let Some(first) = kinds.next() else {
137 tokio::select! {
138 result = session.subscription_closed() => return result,
139 _ = changes.changed() => {},
140 _ = self.lifecycle.draining(), if !draining => {},
141 }
142 continue;
143 };
144 event.change = if kinds.next().is_none() {
145 first
146 } else {
147 LifecycleChange::Snapshot
148 }
149 .into();
150 }
151 sequence = sequence
152 .checked_add(1)
153 .ok_or_else(|| invalid("WATCH sequence exhausted"))?;
154 event.sequence = sequence;
155 let frame = Control {
156 operation: operation::WATCH,
157 request_id: request.request_id,
158 generation: self.generation,
159 payload: event.encode_to_vec(),
160 ..Default::default()
161 };
162 tokio::time::timeout(Duration::from_secs(5), session.send(&frame))
165 .await
166 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))??;
167 previous = Some(event);
168 }
169 }
170}
171
172pub struct WatchClient<S> {
183 session: wire::AsyncSession<S>,
184 request: Control,
185 latest: LifecycleEvent,
186 failed: bool,
187}
188impl<S: AsyncRead + AsyncWrite + Unpin> WatchClient<S> {
189 pub async fn connect(stream: S, offer: &Hello, deadline: Instant) -> io::Result<Option<Self>> {
191 if offer.supported & capability::WATCH == 0 {
192 return Err(invalid("client offer does not support WATCH"));
193 }
194 tokio::time::timeout_at(deadline, async {
195 let mut session = wire::AsyncSession::connect(stream, offer).await?;
196 if session.agreement().capabilities & capability::WATCH == 0 {
197 return Ok(None);
198 }
199 let request = Control {
200 operation: operation::WATCH,
201 ..Default::default()
202 };
203 session.send(&request).await?;
204 let latest = decode_event(&session.receive().await?, &request)?;
205 if latest.sequence != 1 || latest.change != LifecycleChange::Snapshot {
206 return Err(invalid("WATCH must start with a snapshot"));
207 }
208 Ok(Some(Self {
209 session,
210 request,
211 latest,
212 failed: false,
213 }))
214 })
215 .await
216 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))?
217 }
218
219 pub fn snapshot(&self) -> &LifecycleEvent {
221 &self.latest
222 }
223
224 pub async fn changed(&mut self) -> io::Result<&LifecycleEvent> {
226 if self.failed {
227 return Err(invalid("WATCH failed; reconnect"));
228 }
229 self.failed = true;
230 let event = decode_event(&self.session.receive().await?, &self.request)?;
231 if self.latest.sequence.checked_add(1) != Some(event.sequence)
232 || event.process_id != self.latest.process_id
233 || event.generation != self.latest.generation
234 || event.build != self.latest.build
235 || event.selected_generation < self.latest.selected_generation
236 || (event.selected_generation == self.latest.selected_generation
237 && event.selected_build != self.latest.selected_build)
238 || (self.latest.draining && !event.draining)
239 || (self.latest.retiring && !event.retiring)
240 {
241 return Err(invalid("inconsistent WATCH event"));
242 }
243 self.latest = event;
244 self.failed = false;
245 Ok(&self.latest)
246 }
247}
248
249fn decode_event(reply: &Control, request: &Control) -> io::Result<LifecycleEvent> {
250 if reply.kind != MessageKind::Lifecycle
251 || reply.operation != operation::WATCH
252 || reply.request_id != request.request_id
253 || !reply.token.is_empty()
254 || reply.offset.is_some()
255 {
256 return Err(invalid("unexpected WATCH response"));
257 }
258 let event: LifecycleEvent = wire::decode_message(&reply.payload, wire::MAX_FRAME)?;
259 if event.change.as_known().is_none()
260 || event.process_id == 0
261 || event.generation != reply.generation
262 || (event.selected_generation.is_none() && !event.selected_build.is_empty())
263 || (event.selected_generation == Some(event.generation)
264 && event.selected_build != event.build)
265 || (event.change == LifecycleChange::SelectionChanged
266 && event.selected_generation.is_none())
267 || (event.ready && event.draining)
268 || (event.retiring && !event.draining)
269 || (event.change == LifecycleChange::Ready && !event.ready)
270 || (event.change == LifecycleChange::Draining && !event.draining)
271 || (event.change == LifecycleChange::Retiring && !event.retiring)
272 {
273 return Err(invalid("invalid WATCH state"));
274 }
275 Ok(event)
276}