Skip to main content

clawft_kernel/workload_runtime/infer/
lifecycle.rs

1//! Start, stop, unload, status, restart and reconcile for [`InferRuntime`].
2//!
3//! The instances lock is never held across a network call or a process
4//! termination: state is read or taken under the lock, the slow work runs
5//! with the lock dropped, and results are written back under a fresh lock.
6
7use std::net::IpAddr;
8use std::time::{Duration, Instant};
9
10use super::exposure::{port_in_use, reachable_beyond_loopback};
11use super::ollama::{self, LoadState};
12use super::probe::{Health, ServerClient, ServerReport};
13use super::runtime::{InferRuntime, Instance, Managed, ManagedPlan};
14use super::spec::InferFlavor;
15use crate::workload_runtime::evidence::RunEvidence;
16use crate::workload_runtime::supervise::{LaunchSpec, ProcLimits, Supervised};
17use crate::workload_runtime::types::{
18    InstanceHandle, InstanceState, InstanceStatus, RuntimeError, WorkloadRuntime,
19};
20
21/// Output kept per stream from a model server (bytes).
22const OUTPUT_LIMIT: usize = 1024 * 1024;
23/// Grace given to a server when its instance is unloaded.
24const UNLOAD_GRACE: Duration = Duration::from_secs(5);
25/// Timeout of an Ollama load request (a large model can take minutes).
26const OLLAMA_LOAD_TIMEOUT: Duration = Duration::from_secs(900);
27
28/// What [`InferRuntime::reconcile`] did.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub enum Reconcile {
31    /// Running and answering, or not wanted running.
32    Healthy,
33    /// Not a supervised process (adopted, or Ollama).
34    NotManaged,
35    /// It died; the restart waits out its backoff.
36    Backoff {
37        /// Time left before the next restart is allowed.
38        retry_in: Duration,
39    },
40    /// It was restarted.
41    Restarted {
42        /// Restarts so far, this one included.
43        attempt: u32,
44    },
45    /// The restart budget is spent; the instance stays `Exited`.
46    GaveUp {
47        /// Restarts attempted.
48        attempts: u32,
49    },
50    /// The server was listening beyond loopback, so it was stopped. The
51    /// caller chains this; the instance stays down until reloaded.
52    StoppedExposed {
53        /// Local addresses the port answered on.
54        reachable_on: Vec<IpAddr>,
55    },
56}
57
58fn unknown(h: &InstanceHandle) -> RuntimeError {
59    RuntimeError::UnknownInstance(h.instance_id.clone())
60}
61
62fn unlimited() -> ProcLimits {
63    // Model servers map tens of GiB of weights and many files; the cog
64    // limits do not apply. Large finite values: an explicit RLIM_INFINITY
65    // is refused by some kernels when set as the hard limit.
66    ProcLimits {
67        mem_bytes: 1 << 46,
68        cpu_secs: None,
69        nofile: 4096,
70        fsize_bytes: 1 << 44,
71    }
72}
73
74fn spawn(rt: &InferRuntime, inst: &mut Instance) -> Result<(), RuntimeError> {
75    let run_as = rt.managed_cfg().and_then(|m| m.run_as);
76    let Some(Managed {
77        plan: ManagedPlan::Process(l),
78        proc,
79        wanted,
80        exited_at,
81        started_at,
82        exposed,
83        ..
84    }) = inst.managed.as_mut()
85    else {
86        return Err(RuntimeError::Unsupported("not a supervised process".into()));
87    };
88    if let Some(a) = exposed {
89        return Err(RuntimeError::InvalidState(format!(
90            "the server was stopped for listening beyond loopback ({a:?}); reload the instance"
91        )));
92    }
93    *proc = Some(Supervised::spawn(LaunchSpec {
94        program: l.program.clone(),
95        args: l.args.clone(),
96        env: l.env.clone(),
97        cwd: l.dir.clone(),
98        limits: unlimited(),
99        output_limit: OUTPUT_LIMIT,
100        run_as,
101    })?);
102    *wanted = true;
103    *exited_at = None;
104    *started_at = Some(Instant::now());
105    Ok(())
106}
107
108/// Under the lock: if the process exited on its own, take it (and its exit
109/// code) so it can be collected after the lock is dropped.
110fn take_exited(mg: &mut Managed) -> Option<(Supervised, Option<i32>)> {
111    let code = mg.proc.as_mut()?.try_exit()?.code();
112    mg.exited_at.get_or_insert_with(Instant::now);
113    Some((mg.proc.take()?, code))
114}
115
116fn map_report(
117    flavor: InferFlavor,
118    model: Option<&str>,
119    r: &ServerReport,
120    alive: bool,
121    adopted: bool,
122    load: Option<&LoadState>,
123) -> InstanceStatus {
124    let degraded = |d: String| InstanceStatus {
125        state: InstanceState::Degraded,
126        exit_code: None,
127        detail: Some(d),
128    };
129    let running = |d: &str| InstanceStatus {
130        detail: Some(d.into()),
131        ..InstanceStatus::of(InstanceState::Running)
132    };
133    match &r.health {
134        Health::Loading => degraded("loading model".into()),
135        Health::Unhealthy(w) => degraded(w.clone()),
136        Health::Unreachable(w) if alive => degraded(format!("starting: not listening yet ({w})")),
137        Health::Unreachable(w) => InstanceStatus {
138            state: InstanceState::Exited,
139            exit_code: None,
140            detail: Some(if adopted {
141                format!("unreachable ({w}); adopted servers are not restarted")
142            } else {
143                format!("unreachable ({w})")
144            }),
145        },
146        Health::Up => match (flavor, model) {
147            (InferFlavor::Ollama, Some(m)) => {
148                if !r.lists(m) {
149                    degraded(format!("model {m} is not pulled"))
150                } else if r.resident(m) {
151                    running("model resident")
152                } else if let Some(LoadState::Failed(e)) = load {
153                    degraded(format!("load failed: {e}"))
154                } else if matches!(load, Some(LoadState::Loading)) {
155                    degraded("loading model".into())
156                } else if adopted {
157                    running("server up; model not resident")
158                } else {
159                    InstanceStatus::of(InstanceState::Loaded)
160                }
161            }
162            (_, Some(m)) if !r.models.is_empty() && !r.lists(m) => {
163                degraded(format!("model {m} is not listed by the server"))
164            }
165            _ => InstanceStatus::of(InstanceState::Running),
166        },
167    }
168}
169
170impl InferRuntime {
171    fn rt_id(&self) -> String {
172        WorkloadRuntime::id(self).to_string()
173    }
174
175    /// Write back the evidence of a process collected outside the lock.
176    async fn store_last(&self, h: &InstanceHandle, mut ev: RunEvidence) {
177        ev.runtime = self.rt_id();
178        ev.instance_id = h.instance_id.clone();
179        if let Some(mg) = self
180            .instances
181            .lock()
182            .await
183            .get_mut(&h.instance_id)
184            .and_then(|i| i.managed.as_mut())
185        {
186            mg.last = Some(ev);
187        }
188    }
189
190    pub(super) async fn start_instance(&self, h: &InstanceHandle) -> Result<(), RuntimeError> {
191        // Phase 1: what to do, under the lock.
192        let (client, spec, supervised) = {
193            let g = self.instances.lock().await;
194            let inst = g.get(&h.instance_id).ok_or_else(|| unknown(h))?;
195            if let Some(a) = inst.managed.as_ref().and_then(|m| m.exposed.as_ref()) {
196                return Err(RuntimeError::InvalidState(format!(
197                    "the server was stopped for listening beyond loopback ({a:?}); reload the instance"
198                )));
199            }
200            let supervised = inst
201                .managed
202                .as_ref()
203                .map(|m| matches!(m.plan, ManagedPlan::Process(_)));
204            (inst.client.clone(), inst.spec.clone(), supervised)
205        };
206        match supervised {
207            None => {
208                // Adopted: nothing to start; confirm the server answers.
209                return match client.probe(spec.runtime).await.health {
210                    Health::Unreachable(w) => Err(RuntimeError::Backend(format!(
211                        "adopted server is not answering ({w}); WeftOS does not start adopted servers"
212                    ))),
213                    _ => Ok(()),
214                };
215            }
216            Some(true) => {
217                // Phase 2: never start over something already listening.
218                let (ip, port) = (spec.bind_ip()?, spec.port()?);
219                if port_in_use(ip, port).await {
220                    return Err(RuntimeError::Backend(format!(
221                        "{ip}:{port} is already taken by something else; not starting over it"
222                    )));
223                }
224            }
225            Some(false) => {}
226        }
227        // Unified-memory budget and co-residency (managed only): refuse
228        // before anything is spawned.
229        let mut reserved_new = false;
230        if supervised.is_some()
231            && let Some(ledger) = self.managed_cfg().and_then(|m| m.ledger.clone())
232        {
233            match ledger.reserve(&h.instance_id, &spec) {
234                Ok(newly) => reserved_new = newly,
235                Err(e) => return Err(RuntimeError::AdmissionRefused(e.to_string())),
236            }
237        }
238        let result: Result<(), RuntimeError> = async {
239            // Phase 3: act, under the lock (no awaits).
240            let mut g = self.instances.lock().await;
241            let inst = g.get_mut(&h.instance_id).ok_or_else(|| unknown(h))?;
242            let Some(mg) = inst.managed.as_mut() else {
243                return Err(unknown(h));
244            };
245            match &mg.plan {
246                ManagedPlan::Process(_) => {
247                    if mg.proc.as_mut().is_some_and(|p| p.try_exit().is_none()) {
248                        return Err(RuntimeError::InvalidState("already running".into()));
249                    }
250                    mg.proc = None;
251                    spawn(self, inst)
252                }
253                ManagedPlan::Ollama { tag } => {
254                    if matches!(&mg.load, Some((s, _))
255                        if matches!(*s.lock().unwrap_or_else(|e| e.into_inner()), LoadState::Loading))
256                    {
257                        return Err(RuntimeError::InvalidState("already loading".into()));
258                    }
259                    let keep = self
260                        .managed_cfg()
261                        .map_or("5m".to_string(), |m| m.keep_alive.clone());
262                    let client =
263                        ServerClient::new(inst.client.base().to_string(), OLLAMA_LOAD_TIMEOUT)?;
264                    let state = std::sync::Arc::new(std::sync::Mutex::new(LoadState::Loading));
265                    let (s2, tag) = (state.clone(), tag.clone());
266                    let we_loaded = mg.we_loaded.clone();
267                    let ledger = self.managed_cfg().and_then(|m| m.ledger.clone());
268                    let instance = h.instance_id.clone();
269                    let task = tokio::spawn(async move {
270                        // A model somebody else already holds in memory is not
271                        // ours to load or to unload later.
272                        let r = if ollama::resident(&client, &tag).await {
273                            we_loaded.store(false, std::sync::atomic::Ordering::SeqCst);
274                            Ok(())
275                        } else {
276                            // Ours from the moment we ask, so a load that is
277                            // aborted half way is still unloaded at stop.
278                            we_loaded.store(true, std::sync::atomic::Ordering::SeqCst);
279                            let r = ollama::load(&client, &tag, &keep).await;
280                            if r.is_err() {
281                                we_loaded.store(false, std::sync::atomic::Ordering::SeqCst);
282                            }
283                            r
284                        };
285                        if r.is_err()
286                            && let Some(l) = ledger
287                        {
288                            // A failed load holds no memory.
289                            l.release(&instance);
290                        }
291                        *s2.lock().unwrap_or_else(|e| e.into_inner()) =
292                            r.map_or_else(LoadState::Failed, |()| LoadState::Done);
293                    });
294                    mg.load = Some((state, task));
295                    mg.wanted = true;
296                    Ok(())
297                }
298            }
299        }
300        .await;
301        if result.is_err()
302            && reserved_new
303            && let Some(ledger) = self.managed_cfg().and_then(|m| m.ledger.clone())
304        {
305            ledger.release(&h.instance_id);
306        }
307        result
308    }
309
310    /// Give a managed instance a fresh restart budget. Only an explicit
311    /// operator start calls this: an automatic restart or drive never does,
312    /// so a server that keeps dying stays given up.
313    pub async fn reset_restarts(&self, h: &InstanceHandle) {
314        if let Some(mg) = self
315            .instances
316            .lock()
317            .await
318            .get_mut(&h.instance_id)
319            .and_then(|i| i.managed.as_mut())
320        {
321            mg.restarts = 0;
322        }
323    }
324
325    /// Give back the instance's budget (no-op without a ledger).
326    fn release_residency(&self, h: &InstanceHandle) {
327        if let Some(l) = self.managed_cfg().and_then(|m| m.ledger.as_ref()) {
328            l.release(&h.instance_id);
329        }
330    }
331
332    /// Stop the instance. For Ollama this unloads the model from Ollama's
333    /// memory when this adapter loaded it (a model that was already
334    /// resident belongs to whoever loaded it): Ollama
335    /// has no per-client ownership of a loaded model.
336    pub(super) async fn stop_instance(
337        &self,
338        h: &InstanceHandle,
339        grace: Duration,
340    ) -> Result<RunEvidence, RuntimeError> {
341        enum Todo {
342            Proc(Supervised),
343            Ollama(ServerClient, String, bool),
344        }
345        let todo = {
346            let mut g = self.instances.lock().await;
347            let inst = g.get_mut(&h.instance_id).ok_or_else(|| unknown(h))?;
348            let client = inst.client.clone();
349            let Some(mg) = inst.managed.as_mut() else {
350                return Err(RuntimeError::Unsupported(format!(
351                    "{} is adopted: WeftOS observes it and never stops it; unload forgets the registration",
352                    h.instance_id
353                )));
354            };
355            mg.wanted = false;
356            match &mg.plan {
357                ManagedPlan::Process(_) => Todo::Proc(
358                    mg.proc
359                        .take()
360                        .ok_or_else(|| RuntimeError::InvalidState("not running".into()))?,
361                ),
362                ManagedPlan::Ollama { tag } => {
363                    if let Some((_, t)) = mg.load.take() {
364                        t.abort();
365                    }
366                    Todo::Ollama(
367                        client,
368                        tag.clone(),
369                        mg.we_loaded.swap(false, std::sync::atomic::Ordering::SeqCst),
370                    )
371                }
372            }
373        };
374        // The memory is given back only once the server is really gone.
375        match todo {
376            Todo::Proc(p) => {
377                let ev = p.terminate(grace).await;
378                self.release_residency(h);
379                if let Some(mg) = self
380                    .instances
381                    .lock()
382                    .await
383                    .get_mut(&h.instance_id)
384                    .and_then(|i| i.managed.as_mut())
385                {
386                    mg.exited_at = Some(Instant::now());
387                }
388                self.store_last(h, ev.clone()).await;
389                let mut ev = ev;
390                ev.runtime = self.rt_id();
391                ev.instance_id = h.instance_id.clone();
392                Ok(ev)
393            }
394            Todo::Ollama(client, tag, ours) => {
395                // Only a model this adapter loaded is taken out of memory.
396                if ours {
397                    ollama::unload(&client, &tag)
398                        .await
399                        .map_err(RuntimeError::Backend)?;
400                }
401                self.release_residency(h);
402                Ok(RunEvidence {
403                    runtime: self.rt_id(),
404                    instance_id: h.instance_id.clone(),
405                    ..RunEvidence::default()
406                })
407            }
408        }
409    }
410
411    /// Forget the instance. A managed process it spawned is stopped; for
412    /// Ollama the model is unloaded (even if another client loaded it).
413    pub(super) async fn unload_instance(&self, h: &InstanceHandle) -> Result<(), RuntimeError> {
414        let mut inst = self
415            .instances
416            .lock()
417            .await
418            .remove(&h.instance_id)
419            .ok_or_else(|| unknown(h))?;
420        if let Some(mg) = inst.managed.as_mut() {
421            // Only ever a process this adapter spawned itself.
422            if let Some(p) = mg.proc.take() {
423                p.terminate(UNLOAD_GRACE).await;
424            }
425            if let Some((_, t)) = mg.load.take() {
426                t.abort();
427            }
428            match &mg.plan {
429                ManagedPlan::Process(l) => {
430                    let _ = std::fs::remove_dir_all(&l.dir);
431                }
432                ManagedPlan::Ollama { tag } => {
433                    // Best effort, and only a model this adapter loaded.
434                    if mg.we_loaded.swap(false, std::sync::atomic::Ordering::SeqCst) {
435                        let _ = ollama::unload(&inst.client, tag).await;
436                    }
437                }
438            }
439        }
440        // The instance is forgotten here, so its memory is released even if
441        // the best-effort unload above failed: there is no handle left to
442        // release it later.
443        self.release_residency(h);
444        Ok(())
445    }
446
447    /// If the instance's server answers on a non-loopback address of this
448    /// machine, stop it (it is our process) and remember why.
449    async fn enforce_loopback(&self, h: &InstanceHandle) -> Option<Vec<IpAddr>> {
450        let port = {
451            let g = self.instances.lock().await;
452            let inst = g.get(&h.instance_id)?;
453            let mg = inst.managed.as_ref()?;
454            (matches!(mg.plan, ManagedPlan::Process(_)) && mg.proc.is_some())
455                .then(|| inst.spec.port().ok())
456                .flatten()?
457        };
458        let beyond = reachable_beyond_loopback(port).await;
459        if beyond.is_empty() {
460            return None;
461        }
462        let p = {
463            let mut g = self.instances.lock().await;
464            let mg = g.get_mut(&h.instance_id)?.managed.as_mut()?;
465            mg.wanted = false;
466            mg.exposed = Some(beyond.clone());
467            mg.exited_at = Some(Instant::now());
468            mg.proc.take()
469        };
470        if let Some(p) = p {
471            let ev = p.terminate(Duration::from_secs(3)).await;
472            self.store_last(h, ev).await;
473        }
474        // Stopped for good (until reloaded): it holds no memory any more.
475        self.release_residency(h);
476        tracing::warn!(instance = %h.instance_id, ?beyond, "model server listened beyond loopback; stopped");
477        Some(beyond)
478    }
479
480    pub(super) async fn status_of(&self, h: &InstanceHandle) -> InstanceStatus {
481        let (client, spec, model, alive, load, adopted) = {
482            let mut g = self.instances.lock().await;
483            let Some(inst) = g.get_mut(&h.instance_id) else {
484                return InstanceStatus::of(InstanceState::Unknown);
485            };
486            let (client, spec) = (inst.client.clone(), inst.spec.clone());
487            match inst.managed.as_mut() {
488                None => {
489                    let m = spec.model.clone();
490                    (client, spec, m, false, None, true)
491                }
492                Some(mg) => {
493                    if let Some(a) = &mg.exposed {
494                        return InstanceStatus {
495                            state: InstanceState::Exited,
496                            exit_code: None,
497                            detail: Some(format!(
498                                "stopped: the server listened beyond loopback (reachable on {a:?})"
499                            )),
500                        };
501                    }
502                    if let Some((p, code)) = take_exited(mg) {
503                        drop(g);
504                        let ev = p.terminate(Duration::ZERO).await;
505                        self.store_last(h, ev).await;
506                        return InstanceStatus {
507                            state: InstanceState::Exited,
508                            exit_code: code,
509                            detail: Some("server process exited".into()),
510                        };
511                    }
512                    let alive = mg.proc.is_some();
513                    if matches!(mg.plan, ManagedPlan::Process(_)) && !alive {
514                        let state = if mg.last.is_some() {
515                            InstanceState::Exited
516                        } else {
517                            InstanceState::Loaded
518                        };
519                        return InstanceStatus {
520                            state,
521                            exit_code: mg.last.as_ref().and_then(|e| e.exit_code),
522                            detail: None,
523                        };
524                    }
525                    let load = mg
526                        .load
527                        .as_ref()
528                        .map(|(s, _)| s.lock().unwrap_or_else(|e| e.into_inner()).clone());
529                    let model = match &mg.plan {
530                        ManagedPlan::Ollama { tag } => tag.clone(),
531                        ManagedPlan::Process(_) => mg.model_name.clone(),
532                    };
533                    (client, spec, Some(model), alive, load, false)
534                }
535            }
536        };
537        // The binding is checked before the (slow) health probe: a server
538        // we started must not answer beyond loopback, whatever its health.
539        if alive && let Some(a) = self.enforce_loopback(h).await {
540            return InstanceStatus {
541                state: InstanceState::Exited,
542                exit_code: None,
543                detail: Some(format!(
544                    "stopped: the server listened beyond loopback (reachable on {a:?})"
545                )),
546            };
547        }
548        let r = client.probe(spec.runtime).await;
549        map_report(
550            spec.runtime,
551            model.as_deref(),
552            &r,
553            alive,
554            adopted,
555            load.as_ref(),
556        )
557    }
558
559    /// Stop (when running) then start again.
560    pub async fn restart(&self, h: &InstanceHandle, grace: Duration) -> Result<(), RuntimeError> {
561        match self.stop_instance(h, grace).await {
562            Ok(_) | Err(RuntimeError::InvalidState(_)) => {}
563            Err(e) => return Err(e),
564        }
565        self.start_instance(h).await
566    }
567
568    /// Restart a managed server that died while it should be running,
569    /// under the configured backoff and budget, and stop one found
570    /// listening beyond loopback. Meant to be called on a timer; each call
571    /// does at most one restart.
572    pub async fn reconcile(&self, h: &InstanceHandle) -> Result<Reconcile, RuntimeError> {
573        let Some(policy) = self.managed_cfg().map(|m| m.restart) else {
574            return Ok(Reconcile::NotManaged);
575        };
576        enum Next {
577            Done(Reconcile),
578            Restart {
579                ip: IpAddr,
580                port: u16,
581            },
582            Check {
583                client: ServerClient,
584                flavor: InferFlavor,
585            },
586        }
587        let reaped;
588        let next = {
589            let mut g = self.instances.lock().await;
590            let inst = g.get_mut(&h.instance_id).ok_or_else(|| unknown(h))?;
591            let (client, flavor) = (inst.client.clone(), inst.spec.runtime);
592            let (ip, port) = (inst.spec.bind_ip()?, inst.spec.port()?);
593            let Some(mg) = inst.managed.as_mut() else {
594                return Ok(Reconcile::NotManaged);
595            };
596            if !matches!(mg.plan, ManagedPlan::Process(_)) {
597                return Ok(Reconcile::NotManaged);
598            }
599            if let Some(a) = &mg.exposed {
600                let reachable_on = a.clone();
601                drop(g);
602                self.release_residency(h);
603                return Ok(Reconcile::StoppedExposed { reachable_on });
604            }
605            if !mg.wanted {
606                return Ok(Reconcile::Healthy);
607            }
608            reaped = take_exited(mg).map(|(p, _)| p);
609            if mg.proc.is_some() {
610                Next::Check { client, flavor }
611            } else if mg.restarts >= policy.max_restarts {
612                Next::Done(Reconcile::GaveUp {
613                    attempts: mg.restarts,
614                })
615            } else {
616                let wait = policy.delay(mg.restarts);
617                let since = mg.exited_at.map_or(wait, |t| t.elapsed());
618                if since < wait {
619                    Next::Done(Reconcile::Backoff {
620                        retry_in: wait - since,
621                    })
622                } else {
623                    Next::Restart { ip, port }
624                }
625            }
626        };
627        if let Some(p) = reaped {
628            let ev = p.terminate(Duration::ZERO).await;
629            self.store_last(h, ev).await;
630        }
631        match next {
632            Next::Done(r) => {
633                // Out of restarts: nothing is running, so nothing is held.
634                if matches!(r, Reconcile::GaveUp { .. }) {
635                    self.release_residency(h);
636                }
637                Ok(r)
638            }
639            Next::Restart { ip, port } => {
640                if port_in_use(ip, port).await {
641                    return Err(RuntimeError::Backend(format!(
642                        "{ip}:{port} is already taken by something else; not restarting over it"
643                    )));
644                }
645                let mut g = self.instances.lock().await;
646                let inst = g.get_mut(&h.instance_id).ok_or_else(|| unknown(h))?;
647                let attempt = {
648                    let mg = inst.managed.as_mut().ok_or_else(|| unknown(h))?;
649                    mg.restarts += 1;
650                    mg.restarts
651                };
652                spawn(self, inst)?;
653                Ok(Reconcile::Restarted { attempt })
654            }
655            Next::Check { client, flavor } => {
656                if let Some(a) = self.enforce_loopback(h).await {
657                    return Ok(Reconcile::StoppedExposed { reachable_on: a });
658                }
659                let up = client.probe(flavor).await.health;
660                // Healthy for a while, not just once: a server that answers
661                // a probe and then dies keeps its restart count, so a crash
662                // loop still reaches `GaveUp`.
663                if up == Health::Up
664                    && let Some(mg) = self
665                        .instances
666                        .lock()
667                        .await
668                        .get_mut(&h.instance_id)
669                        .and_then(|i| i.managed.as_mut())
670                    && mg.started_at.is_some_and(|t| t.elapsed() >= policy.stable_after)
671                {
672                    mg.restarts = 0;
673                }
674                Ok(Reconcile::Healthy)
675            }
676        }
677    }
678}