Skip to main content

liminal_server/services/site/
processes.rs

1//! Process state changes share the lifecycle registry.
2
3use super::{
4    Lifecycle, LifecycleError, ProcessGrant, ServiceRecord, ServiceState, Transition, revoke, run,
5};
6use std::sync::Arc;
7
8impl Lifecycle {
9    /// Starts an asleep service's template for `holder`; it is recorded
10    /// live when its worker registers.
11    ///
12    /// # Errors
13    /// By name: the service, its state, or the start.
14    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    /// Starts a held service's template again for `holder`.
23    ///
24    /// # Errors
25    /// As [`Self::deploy`].
26    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(&registry, &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    /// Stops a running or live service for `holder` and records it held.
70    ///
71    /// # Errors
72    /// By name: the service, its state, the stop or the store.
73    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    /// Stops a service if it runs and records it retired, for good.
83    ///
84    /// # Errors
85    /// As [`Self::hold`].
86    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            // A site under construction runs nothing: it can be abandoned
112            // (retired) but there is nothing to hold.
113            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    /// What `token` admits, when it is a running service process's own.
203    #[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    /// Records `id` live when its worker registered, for the holder that
210    /// started it; nothing when it already is.
211    ///
212    /// # Errors
213    /// The service is unknown or the store failed.
214    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    /// Starts every service last recorded live, as the holder that last
244    /// made it so, after a restart. A start that fails is recorded on the
245    /// service and the rest still start.
246    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    /// The server's shutdown has begun: from here no service starts, and a
283    /// process that exits (a worker leaves when its connection closes) keeps
284    /// its service's recorded state, so a live service is live at the next
285    /// start. Called when the stop is asked, before any connection drains.
286    pub fn shutdown_begun(&self) {
287        if let Ok(mut registry) = self.lock() {
288            registry.stopping_all = true;
289        }
290    }
291
292    /// Stops every running process at server shutdown without changing a
293    /// state, so a live service is started again by the next start.
294    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    /// A process exited: unasked, it is recorded on its service; a stop that
321    /// asked records its own state, and so does the server's shutdown once it
322    /// has begun.
323    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}