liminal_server/services/site/
processes.rs1use super::{
4 Lifecycle, LifecycleError, ProcessGrant, ServiceRecord, ServiceState, Transition, revoke, run,
5};
6use std::sync::Arc;
7
8impl Lifecycle {
9 pub fn deploy(
15 self: &Arc<Self>,
16 id: &str,
17 holder: &str,
18 ) -> Result<ServiceRecord, LifecycleError> {
19 self.start(id, holder, "deploy", ServiceState::Asleep)
20 }
21
22 pub fn wake(self: &Arc<Self>, id: &str, holder: &str) -> Result<ServiceRecord, LifecycleError> {
27 self.start(id, holder, "wake", ServiceState::Held)
28 }
29
30 fn start(
31 self: &Arc<Self>,
32 id: &str,
33 holder: &str,
34 verb: &'static str,
35 from: ServiceState,
36 ) -> Result<ServiceRecord, LifecycleError> {
37 let mut registry = self.lock()?;
38 let record = registry
39 .records
40 .get(id)
41 .cloned()
42 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
43 let running = registry.running.contains_key(id);
44 if record.state != from || running || registry.stopping_all {
45 return Err(LifecycleError::NotNow {
46 verb,
47 state: record.state,
48 running,
49 });
50 }
51 let template = self.template_for(®istry, &record)?;
52 let approved = record
53 .template
54 .as_ref()
55 .and_then(|name| registry.construction.templates.get(name));
56 let (process, digest) = run::start(self, &record, &template, approved, holder)?;
57 registry.tokens.insert(
58 digest,
59 ProcessGrant {
60 service: record.id.clone(),
61 subjects: record.subjects.clone(),
62 },
63 );
64 registry.running.insert(record.id.clone(), process);
65 drop(registry);
66 Ok(record)
67 }
68
69 pub fn hold(
74 &self,
75 id: &str,
76 holder: &str,
77 now_ms: u64,
78 ) -> Result<ServiceRecord, LifecycleError> {
79 self.stop(id, holder, now_ms, "hold", ServiceState::Held)
80 }
81
82 pub fn retire(
87 &self,
88 id: &str,
89 holder: &str,
90 now_ms: u64,
91 ) -> Result<ServiceRecord, LifecycleError> {
92 self.stop(id, holder, now_ms, "retire", ServiceState::Retired)
93 }
94
95 fn stop(
96 &self,
97 id: &str,
98 holder: &str,
99 now_ms: u64,
100 verb: &'static str,
101 to: ServiceState,
102 ) -> Result<ServiceRecord, LifecycleError> {
103 let (state, stopping) = {
104 let mut registry = self.lock()?;
105 let state = registry
106 .records
107 .get(id)
108 .map(|record| record.state)
109 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
110 let running = registry.running.contains_key(id);
111 if state == ServiceState::Building && to != ServiceState::Retired {
114 return Err(LifecycleError::NotNow {
115 verb,
116 state,
117 running,
118 });
119 }
120 let applies = match to {
121 ServiceState::Held => state == ServiceState::Live || running,
122 _ => state != ServiceState::Retired,
123 };
124 if !applies {
125 return Err(LifecycleError::NotNow {
126 verb,
127 state,
128 running,
129 });
130 }
131 let process = revoke(&mut registry, id);
132 drop(registry);
133 (state, process)
134 };
135 if let Some(process) = stopping {
136 run::stop(&process, self.timeout_of(id)?).map_err(LifecycleError::Process)?;
137 }
138 self.transition(id, state, to, holder, now_ms, None)
139 }
140
141 fn timeout_of(&self, id: &str) -> Result<u64, LifecycleError> {
142 let registry = self.lock()?;
143 let record = registry
144 .records
145 .get(id)
146 .ok_or_else(|| LifecycleError::UnknownService(id.into()))?;
147 let name = record
148 .template
149 .as_ref()
150 .ok_or_else(|| LifecycleError::UnknownTemplate("none".into()))?;
151 if let Some(template) = registry.construction.templates.get(name) {
152 return Ok(template.proposal.stop_timeout_ms);
153 }
154 let name = name.clone();
155 drop(registry);
156 self.templates
157 .get(&name)
158 .map(|template| template.stop_timeout_ms)
159 .ok_or_else(|| LifecycleError::UnknownTemplate(name.clone()))
160 }
161
162 fn transition(
163 &self,
164 id: &str,
165 from: ServiceState,
166 to: ServiceState,
167 holder: &str,
168 now_ms: u64,
169 note: Option<String>,
170 ) -> Result<ServiceRecord, LifecycleError> {
171 let mut registry = self.lock()?;
172 let mut record = registry
173 .records
174 .get(id)
175 .cloned()
176 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
177 if record.purpose.is_some()
178 && record.transitions.len()
179 >= self
180 .site_bindings()
181 .map_err(|error| LifecycleError::Store(error.to_string()))?
182 .limits
183 .transitions
184 .get()
185 {
186 return Err(LifecycleError::Store("transitions_bound_exceeded".into()));
187 }
188 record.state = to;
189 record.transitions.push(Transition {
190 from,
191 to,
192 holder: holder.to_owned(),
193 at_ms: now_ms,
194 note,
195 });
196 self.keep(&mut registry, &record)?;
197 registry.records.insert(id.to_owned(), record.clone());
198 drop(registry);
199 Ok(record)
200 }
201
202 #[must_use]
204 pub fn admit_process(&self, token: &[u8]) -> Option<ProcessGrant> {
205 let digest = *blake3::hash(token).as_bytes();
206 self.lock().ok()?.tokens.get(&digest).cloned()
207 }
208
209 pub fn mark_live(&self, id: &str, now_ms: u64) -> Result<(), LifecycleError> {
215 let (state, starter) = {
216 let registry = self.lock()?;
217 let state = registry
218 .records
219 .get(id)
220 .map(|record| record.state)
221 .ok_or_else(|| LifecycleError::UnknownService(id.to_owned()))?;
222 let starter = registry
223 .running
224 .get(id)
225 .map_or_else(|| "process".to_owned(), |process| process.starter.clone());
226 drop(registry);
227 (state, starter)
228 };
229 if state == ServiceState::Building {
230 return Err(LifecycleError::NotNow {
231 verb: "mark_live",
232 state,
233 running: false,
234 });
235 }
236 if state == ServiceState::Live {
237 return Ok(());
238 }
239 self.transition(id, state, ServiceState::Live, &starter, now_ms, None)
240 .map(|_| ())
241 }
242
243 pub fn restart_live(self: &Arc<Self>, now_ms: u64) -> Vec<(String, LifecycleError)> {
247 let live: Vec<(String, String)> = self.lock().map_or_else(
248 |_| Vec::new(),
249 |registry| {
250 registry
251 .records
252 .values()
253 .filter(|record| record.state == ServiceState::Live)
254 .map(|record| {
255 let starter = record
256 .transitions
257 .last()
258 .map_or_else(|| record.created_by.clone(), |t| t.holder.clone());
259 (record.id.clone(), starter)
260 })
261 .collect()
262 },
263 );
264 let mut failed = Vec::new();
265 for (id, starter) in live {
266 if let Err(error) = self.start(&id, &starter, "restart", ServiceState::Live) {
267 let note = Some(format!("restart failed: {error}"));
268 let _kept = self.transition(
269 &id,
270 ServiceState::Live,
271 ServiceState::Asleep,
272 "process",
273 now_ms,
274 note,
275 );
276 failed.push((id, error));
277 }
278 }
279 failed
280 }
281
282 pub fn shutdown_begun(&self) {
287 if let Ok(mut registry) = self.lock() {
288 registry.stopping_all = true;
289 }
290 }
291
292 pub fn shutdown(&self) {
295 self.shutdown_begun();
296 let ids: Vec<String> = match self.lock() {
297 Ok(registry) => registry.running.keys().cloned().collect(),
298 Err(_) => return,
299 };
300 for id in ids {
301 let timeout = match self.timeout_of(&id) {
302 Ok(timeout) => timeout,
303 Err(error) => {
304 tracing::error!(service = %id, %error, "service_stop_policy_unavailable");
305 return;
306 }
307 };
308 let process = match self.lock() {
309 Ok(mut registry) => revoke(&mut registry, &id),
310 Err(_) => return,
311 };
312 if let Some(process) = process
313 && let Err(error) = run::stop(&process, timeout)
314 {
315 tracing::warn!(service = %id, error, "a service process did not stop at shutdown");
316 }
317 }
318 }
319
320 pub(in crate::services) fn exited(&self, id: &str, status: &str, now_ms: u64) {
324 let (asked, stopping) = match self.lock() {
325 Ok(mut registry) => {
326 let asked = registry.running.remove(id).is_none_or(|process| {
327 registry.tokens.remove(&process.digest);
328 false
329 });
330 self.changed.notify_all();
331 (asked, registry.stopping_all)
332 }
333 Err(_) => return,
334 };
335 if asked {
336 return;
337 }
338 if stopping {
339 tracing::info!(
340 service = id,
341 status,
342 "a service process exited during shutdown; its state stays"
343 );
344 return;
345 }
346 let Ok(record) = self.record(id) else {
347 return;
348 };
349 let note = Some(format!("exited unasked: {status}"));
350 if let Err(error) = self.transition(
351 id,
352 record.state,
353 ServiceState::Asleep,
354 "process",
355 now_ms,
356 note,
357 ) {
358 tracing::warn!(service = id, %error, "a service exit could not be recorded");
359 }
360 }
361}