Skip to main content

runnerd_core/scheduler/engine/
control.rs

1//! Control from the API: the `Control` handle the daemon's API holds (pause flags, which it records in the store so
2//! a restart keeps them, shutdown, a channel of `Command`s) and the engine's side of each command: pause and resume,
3//! of one repository or all, with or without cancelling their runs, reseed, cache clean and config replacement.
4//! Commands run between ticks or while a tick waits for GitHub, so they see the slot state a tick does (ADRs 0019,
5//! 0026). It must not serve HTTP or write the config file: the daemon does, through `Commit`.
6
7use std::collections::{BTreeMap, BTreeSet};
8use std::sync::Mutex;
9use std::time::Duration;
10
11use tokio::sync::{Notify, mpsc, oneshot};
12use tracing::{info, warn};
13
14use super::{Engine, Occupant};
15use crate::api;
16use crate::cache;
17use crate::config::Config;
18use crate::docker::DockerApi;
19use crate::github::GithubApi;
20use crate::isolation;
21use crate::scheduler::SlotRef;
22use crate::store::Store;
23use crate::sync::lock;
24
25/// How long the runner of a run a forced pause cancelled gets to report its job cancelled and exit, before it is
26/// removed: GitHub completes a cancelled run only once its runner reports it, which took 40s in a test.
27pub(super) const CANCEL_GRACE: Duration = Duration::from_secs(90);
28
29/// How the daemon is asked to stop.
30#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
31pub enum Shutdown {
32    /// Keep scheduling.
33    No,
34    /// Start nothing new, let running jobs finish.
35    Graceful,
36    /// Kill running jobs and delete their runners.
37    Force,
38}
39
40/// What a pause, a shutdown or a runner's removal does with the jobs runners are running.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum RunningJobs {
43    /// Leave them running: a pause refuses, a shutdown waits for them, a busy runner stays.
44    Keep,
45    /// Kill them: a pause cancels their runs, a shutdown and a removal kill their runners.
46    Kill,
47}
48
49/// The repositories a pause or a resume applies to.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum Target {
52    /// One repository, `owner/name`.
53    Repository(String),
54    /// Every configured repository.
55    All,
56}
57
58/// Why the engine refused or failed a command from the API.
59#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
60pub enum ControlError {
61    /// No such repository, profile or cache.
62    #[error("{0}")]
63    NotFound(String),
64    /// Runners use what the command would change.
65    #[error("{message}")]
66    Busy {
67        /// Names the runners and their jobs.
68        message: String,
69        /// The runners in the way.
70        jobs: Vec<api::RunningJob>,
71    },
72    /// The command conflicts with one already in progress.
73    #[error("{0}")]
74    Conflict(String),
75    /// The command cannot apply to this target.
76    #[error("{0}")]
77    Invalid(String),
78    /// Docker or GitHub failed.
79    #[error("{0}")]
80    Failed(String),
81    /// A pause or resume holds in memory, but the job history could not record it: a restart undoes it.
82    #[error("{0}")]
83    NotRecorded(String),
84    /// The engine stopped.
85    #[error("runnerd is stopping")]
86    Stopped,
87}
88
89/// A command the engine carries out between ticks or while a tick waits for GitHub, with where its answer goes.
90#[derive(Debug)]
91pub enum Command {
92    /// Pause one repository or all; with `RunningJobs::Kill`, cancel their running jobs.
93    Pause {
94        /// What to pause.
95        target: Target,
96        /// Cancel running jobs, or refuse while there are any.
97        jobs: RunningJobs,
98        /// The answer.
99        reply: oneshot::Sender<Result<api::Paused, ControlError>>,
100    },
101    /// Resume one repository or all.
102    Resume {
103        /// What to resume.
104        target: Target,
105        /// The answer.
106        reply: oneshot::Sender<Result<(), ControlError>>,
107    },
108    /// Make slot 1's layers the profile's snapshot once its slots are free.
109    Reseed {
110        /// The profile.
111        profile: String,
112        /// The answer.
113        reply: oneshot::Sender<Result<(), ControlError>>,
114    },
115    /// Remove a slot's volume or a shared cache's, unless a runner uses it.
116    CleanCache {
117        /// The volume.
118        name: String,
119        /// The answer.
120        reply: oneshot::Sender<Result<(), ControlError>>,
121    },
122    /// Write a new, validated config to its file and run with it, with no tick in between.
123    ReplaceConfig {
124        /// The config.
125        config: Box<Config>,
126        /// The `MemoryMax` of the slice it names.
127        memory_max: Option<u64>,
128        /// Writes the file.
129        commit: Commit,
130        /// Answered once the engine runs with it: with a warning from the commit, or why it did not commit.
131        reply: oneshot::Sender<Result<Option<String>, ControlError>>,
132    },
133}
134
135/// What makes a new config durable, run by the engine before it runs with the config: `Ok` once the config is in
136/// place, with a warning if it may not survive a crash; `Err` with nothing changed.
137pub struct Commit(Box<dyn FnOnce() -> Result<Option<String>, String> + Send>);
138
139impl Commit {
140    /// A commit that runs `write`, off the engine's thread.
141    pub fn new(write: impl FnOnce() -> Result<Option<String>, String> + Send + 'static) -> Commit {
142        Commit(Box::new(write))
143    }
144
145    /// Runs it.
146    pub async fn run(self) -> Result<Option<String>, String> {
147        tokio::task::spawn_blocking(self.0)
148            .await
149            .unwrap_or_else(|join_error| Err(join_error.to_string()))
150    }
151}
152
153impl std::fmt::Debug for Commit {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        f.write_str("Commit")
156    }
157}
158
159/// The engine's end of `Control`'s commands.
160#[derive(Debug)]
161pub struct Commands(mpsc::UnboundedReceiver<Command>);
162
163impl Commands {
164    /// The next command; `None` once `Control` is gone.
165    pub async fn recv(&mut self) -> Option<Command> {
166        self.0.recv().await
167    }
168}
169
170/// Commands from the API and signals to the loop.
171#[derive(Debug)]
172pub struct Control {
173    // Locked with `sync::lock`: the guarded data stays consistent, as every critical section is a single insert,
174    // remove or read.
175    repositories: Mutex<BTreeSet<String>>,
176    paused: Mutex<BTreeSet<String>>,
177    /// Where `paused` is recorded.
178    store: Store,
179    shutdown: Mutex<Shutdown>,
180    pub(super) wake: Notify,
181    commands: mpsc::UnboundedSender<Command>,
182}
183
184impl Control {
185    /// Controls for the configured repositories, of which those in `paused` start paused, and the commands they
186    /// send the engine. A paused repository the config no longer has is forgotten, in `store` too: one added back
187    /// starts active.
188    pub fn new(
189        repositories: impl IntoIterator<Item = String>,
190        paused: impl IntoIterator<Item = String>,
191        store: Store,
192    ) -> (Control, Commands) {
193        let repositories: BTreeSet<String> = repositories.into_iter().collect();
194        let paused: BTreeSet<String> = paused
195            .into_iter()
196            .filter(|repository| repositories.contains(repository))
197            .collect();
198        // Sent at once; a write that fails leaves rows the next start filters out the same way.
199        drop(store.paused(paused.iter().cloned().collect()));
200        let (commands, receiver) = mpsc::unbounded_channel();
201        let control = Control {
202            repositories: Mutex::new(repositories),
203            paused: Mutex::new(paused),
204            store,
205            shutdown: Mutex::new(Shutdown::No),
206            wake: Notify::new(),
207            commands,
208        };
209        (control, Commands(receiver))
210    }
211
212    /// Whether a shutdown was requested, and how.
213    pub fn mode(&self) -> Shutdown {
214        *lock(&self.shutdown)
215    }
216
217    /// Pauses or resumes one repository or all, whatever they run, and records it in the store before it answers,
218    /// so a restarted daemon starts with the same ones paused. `NotFound` for a repository the config does not have.
219    /// If the record fails, the change holds until a restart and the answer is `NotRecorded`. The status shows it
220    /// at the engine's next publish: the API pauses and resumes through `pause` and `resume`, which publish before
221    /// they answer.
222    pub async fn set_paused(&self, target: &Target, paused: bool) -> Result<(), ControlError> {
223        let recorded = {
224            let repositories = lock(&self.repositories);
225            let names: Vec<&String> = match target {
226                Target::Repository(repository) => {
227                    let Some(name) = repositories.get(repository) else {
228                        return Err(not_configured(repository, repositories.iter()));
229                    };
230                    vec![name]
231                }
232                Target::All => repositories.iter().collect(),
233            };
234            let mut set = lock(&self.paused);
235            for name in names {
236                if paused {
237                    set.insert(name.clone());
238                } else {
239                    set.remove(name);
240                }
241            }
242            // Sent under the lock, so the records change in the order the set does.
243            self.store.paused(set.iter().cloned().collect())
244        };
245        self.wake.notify_one();
246        if recorded.await {
247            return Ok(());
248        }
249        let name = match target {
250            Target::Repository(repository) => repository.as_str(),
251            Target::All => "every repository",
252        };
253        let (done, undone) = if paused {
254            ("paused", "resumes it")
255        } else {
256            ("resumed", "pauses it again")
257        };
258        let message = format!(
259            "{name} is {done} until runnerd restarts, but the job history cannot record it, so a restart {undone}"
260        );
261        warn!("{message}");
262        Err(ControlError::NotRecorded(message))
263    }
264
265    /// Replaces the configured repositories; the removed ones are no longer paused, in the store too.
266    fn set_repositories(&self, repositories: BTreeSet<String>) {
267        let mut configured = lock(&self.repositories);
268        let mut paused = lock(&self.paused);
269        let before = paused.len();
270        paused.retain(|repository| repositories.contains(repository));
271        if paused.len() != before {
272            // Not waited for: a restart forgets a repository the config does not have anyway.
273            drop(self.store.paused(paused.iter().cloned().collect()));
274        }
275        *configured = repositories;
276    }
277
278    /// Asks the loop to stop; a forced request overrides a graceful one, never the other way.
279    pub fn shutdown(&self, jobs: RunningJobs) {
280        let mode = match jobs {
281            RunningJobs::Keep => Shutdown::Graceful,
282            RunningJobs::Kill => Shutdown::Force,
283        };
284        let mut shutdown = lock(&self.shutdown);
285        *shutdown = (*shutdown).max(mode);
286        drop(shutdown);
287        self.wake.notify_one();
288    }
289
290    /// Whether the repository is paused.
291    pub fn is_paused(&self, repository: &str) -> bool {
292        lock(&self.paused).contains(repository)
293    }
294
295    async fn send<T>(
296        &self,
297        command: impl FnOnce(oneshot::Sender<T>) -> Command,
298    ) -> Result<T, ControlError> {
299        let (reply, answer) = oneshot::channel();
300        self.commands
301            .send(command(reply))
302            .map_err(|_| ControlError::Stopped)?;
303        answer.await.map_err(|_| ControlError::Stopped)
304    }
305
306    /// Pauses one repository or all. With `RunningJobs::Keep`, refuses while any of them runs a job, pausing none;
307    /// with `Kill`, cancels the runs of their running jobs on GitHub, whose runners are removed once they report it
308    /// or `CANCEL_GRACE` later, and removes their other runners at once.
309    pub async fn pause(
310        &self,
311        target: Target,
312        jobs: RunningJobs,
313    ) -> Result<api::Paused, ControlError> {
314        self.send(|reply| Command::Pause {
315            target,
316            jobs,
317            reply,
318        })
319        .await?
320    }
321
322    /// Resumes one repository or all, and publishes the status before it answers, so a client that reads the status
323    /// or the events after the answer sees them resumed. Fails as `set_paused` does.
324    pub async fn resume(&self, target: Target) -> Result<(), ControlError> {
325        self.send(|reply| Command::Resume { target, reply }).await?
326    }
327
328    /// Reseeds a profile as soon as its slots are free; none of them starts a runner until then.
329    pub async fn reseed(&self, profile: &str) -> Result<(), ControlError> {
330        self.send(|reply| Command::Reseed {
331            profile: profile.to_owned(),
332            reply,
333        })
334        .await?
335    }
336
337    /// Removes a slot's volume or a shared cache's, unless a runner uses it.
338    pub async fn clean_cache(&self, name: &str) -> Result<(), ControlError> {
339        self.send(|reply| Command::CleanCache {
340            name: name.to_owned(),
341            reply,
342        })
343        .await?
344    }
345
346    /// Runs `commit` to write a validated config, and if it commits, runs with the config from then on, before the
347    /// next tick. If it does not, nothing changes and the answer is `Failed`. Running jobs keep running: removed
348    /// profiles and slots go as their runners finish.
349    pub async fn replace_config(
350        &self,
351        config: Config,
352        memory_max: Option<u64>,
353        commit: Commit,
354    ) -> Result<Option<String>, ControlError> {
355        self.send(|reply| Command::ReplaceConfig {
356            config: Box::new(config),
357            memory_max,
358            commit,
359            reply,
360        })
361        .await?
362    }
363}
364
365/// Names the repositories there are, so a mistyped name shows its fix.
366fn not_configured<'name>(
367    repository: &str,
368    configured: impl Iterator<Item = &'name String>,
369) -> ControlError {
370    let configured: Vec<&str> = configured.map(String::as_str).collect();
371    let configured = if configured.is_empty() {
372        "none is configured".to_owned()
373    } else {
374        format!("configured: {}", configured.join(", "))
375    };
376    ControlError::NotFound(format!(
377        "{repository} is not a configured repository; {configured}"
378    ))
379}
380
381/// `jobs` for people to read: `job 1 on runner-a, runner-b`.
382fn list(jobs: &[api::RunningJob]) -> String {
383    jobs.iter()
384        .map(ToString::to_string)
385        .collect::<Vec<_>>()
386        .join(", ")
387}
388
389impl<G: GithubApi + 'static, D: DockerApi + 'static> Engine<G, D> {
390    /// Carries out a command from the API.
391    pub(super) async fn command(&mut self, command: Command) {
392        match command {
393            Command::Pause {
394                target,
395                jobs,
396                reply,
397            } => {
398                let _ = reply.send(self.pause(&target, jobs).await);
399            }
400            Command::Resume { target, reply } => {
401                let _ = reply.send(self.control.set_paused(&target, false).await);
402            }
403            Command::Reseed { profile, reply } => {
404                let _ = reply.send(self.request_reseed(&profile));
405            }
406            Command::CleanCache { name, reply } => {
407                let _ = reply.send(self.clean_cache(&name).await);
408            }
409            Command::ReplaceConfig {
410                config,
411                memory_max,
412                commit,
413                reply,
414            } => {
415                let answer = match commit.run().await {
416                    Ok(warning) => {
417                        self.reload(*config, memory_max);
418                        Ok(warning)
419                    }
420                    Err(message) => Err(ControlError::Failed(message)),
421                };
422                let _ = reply.send(answer);
423            }
424        }
425    }
426
427    /// Pauses one repository or all. A runner of theirs that holds a job, or that GitHub reports busy, refuses it
428    /// with `RunningJobs::Keep`; with `Kill`, the runs of their jobs are cancelled, and their runners removed once
429    /// they exit or `CANCEL_GRACE` later; a runner whose run cannot be cancelled is removed at once.
430    pub(super) async fn pause(
431        &mut self,
432        target: &Target,
433        running: RunningJobs,
434    ) -> Result<api::Paused, ControlError> {
435        let repositories: BTreeSet<String> = match target {
436            Target::Repository(repository) if !self.repositories.contains_key(repository) => {
437                return Err(not_configured(repository, self.repositories.keys()));
438            }
439            Target::Repository(repository) => BTreeSet::from([repository.clone()]),
440            Target::All => self.repositories.keys().cloned().collect(),
441        };
442        self.take_found();
443        let runners: Vec<(SlotRef, Occupant)> = self
444            .occupied()
445            .filter(|(_, occupant)| repositories.contains(&occupant.repository))
446            .map(|(slot, occupant)| (slot, occupant.clone()))
447            .collect();
448        let jobs: Vec<api::RunningJob> = runners
449            .iter()
450            .filter(|(_, occupant)| occupant.busy || occupant.job.is_some())
451            .map(|(_, occupant)| occupant.running_job())
452            .collect();
453        if running == RunningJobs::Keep && !jobs.is_empty() {
454            let mut by_repository: BTreeMap<&str, Vec<api::RunningJob>> = BTreeMap::new();
455            for job in &jobs {
456                by_repository
457                    .entry(&job.repository)
458                    .or_default()
459                    .push(job.clone());
460            }
461            let runs: Vec<String> = by_repository
462                .iter()
463                .map(|(repository, jobs)| format!("{repository} runs {}", list(jobs)))
464                .collect();
465            // The jobs only: each client says how to force the pause in its own terms.
466            return Err(ControlError::Busy {
467                message: runs.join("; "),
468                jobs,
469            });
470        }
471        // Paused in memory even if not recorded: a forced pause still cancels the runs it was asked to.
472        let recorded = self.control.set_paused(target, true).await;
473        match target {
474            Target::Repository(repository) => info!("{repository}: paused"),
475            Target::All => info!("paused every repository"),
476        }
477        let mut stopped = Vec::new();
478        if running == RunningJobs::Keep {
479            return recorded.map(|()| api::Paused { stopped });
480        }
481        for (slot, occupant) in runners {
482            let repository = &occupant.repository;
483            let cancelled = if let Some(job) = occupant.job {
484                self.cancel(repository, job).await
485            } else {
486                if occupant.busy {
487                    warn!(
488                        "{repository}: {} runs a job its log has not named yet; GitHub fails it once the runner is gone",
489                        occupant.runner_name
490                    );
491                }
492                false
493            };
494            if cancelled {
495                // `collect_finished` removes it once it exits, `kill_cancelled` if it does not.
496                self.cancelling.insert(
497                    occupant.runner_name.clone(),
498                    tokio::time::Instant::now() + CANCEL_GRACE,
499                );
500            } else {
501                self.stop(&slot, RunningJobs::Kill).await;
502                if self
503                    .occupied()
504                    .any(|(_, candidate)| candidate.runner_name == occupant.runner_name)
505                {
506                    continue;
507                }
508            }
509            stopped.push(occupant.running_job());
510        }
511        match recorded {
512            Ok(()) => Ok(api::Paused { stopped }),
513            Err(ControlError::NotRecorded(message)) if !stopped.is_empty() => Err(
514                ControlError::NotRecorded(format!("{message}; stopped {}", list(&stopped))),
515            ),
516            Err(error) => Err(error),
517        }
518    }
519
520    /// Removes the runners of cancelled runs that did not exit within `CANCEL_GRACE`, every tick until they are gone.
521    pub(super) async fn kill_cancelled(&mut self) {
522        let now = tokio::time::Instant::now();
523        let running: BTreeSet<String> = self
524            .occupied()
525            .map(|(_, occupant)| occupant.runner_name.clone())
526            .collect();
527        self.cancelling.retain(|name, _| running.contains(name));
528        let overdue: Vec<(SlotRef, String)> = self
529            .occupied()
530            .filter(|(_, occupant)| {
531                self.cancelling
532                    .get(&occupant.runner_name)
533                    .is_some_and(|deadline| *deadline <= now)
534            })
535            .map(|(slot, occupant)| (slot, occupant.runner_name.clone()))
536            .collect();
537        for (slot, name) in overdue {
538            warn!(
539                "{name} did not exit {}s after its run was cancelled",
540                CANCEL_GRACE.as_secs()
541            );
542            // The deadline stays until the runner is gone, so a failed removal is tried again next tick.
543            self.stop(&slot, RunningJobs::Kill).await;
544        }
545    }
546
547    /// Cancels the run of a job; whether GitHub accepted it.
548    async fn cancel(&self, repository: &str, job: u64) -> bool {
549        let run = match self.github.job(repository, job).await {
550            Ok(details) => details.run_id,
551            Err(error) => {
552                warn!("{repository}: cannot read job {job} to cancel its run: {error}");
553                return false;
554            }
555        };
556        match self.github.cancel_run(repository, run).await {
557            Ok(()) => {
558                info!("{repository}: cancelled run {run} of job {job}");
559                true
560            }
561            Err(error) => {
562                warn!("{repository}: cannot cancel run {run}: {error}");
563                false
564            }
565        }
566    }
567
568    /// Asks for a profile's reseed: none of its slots starts a runner until they are free and it ran.
569    pub(super) fn request_reseed(&mut self, profile: &str) -> Result<(), ControlError> {
570        if !self.config.profiles.contains_key(profile) {
571            return Err(ControlError::NotFound(format!(
572                "{profile} is not a configured profile"
573            )));
574        }
575        if self.is_reseeding(profile) {
576            return Err(ControlError::Conflict(format!(
577                "{profile} is reseeding already"
578            )));
579        }
580        info!("{profile}: reseeding once its slots are free");
581        self.reseed_requested.insert(profile.to_owned());
582        Ok(())
583    }
584
585    /// Removes a slot's volume or a shared cache's, unless a runner or a reseed uses it. Snapshots are rebuilt by
586    /// reseeding instead: slot layers sit on top of them.
587    pub(super) async fn clean_cache(&mut self, name: &str) -> Result<(), ControlError> {
588        let Some(volume) = cache::volumes(&self.names, &self.config)
589            .into_iter()
590            .find(|volume| volume.name == name)
591        else {
592            return Err(ControlError::NotFound(format!(
593                "{name} is not a cache volume of this config; `runnerctl caches` lists them"
594            )));
595        };
596        let users: Vec<api::RunningJob> = match &volume.kind {
597            api::CacheKind::Snapshot { profile } => {
598                return Err(ControlError::Invalid(format!(
599                    "{name} is {profile}'s snapshot, which every slot's layers sit on; reseed {profile} to rebuild it"
600                )));
601            }
602            api::CacheKind::Slot { profile, slot } => {
603                if self.reseeds.contains_key(profile) {
604                    return Err(ControlError::Conflict(format!(
605                        "{profile} is reseeding, which writes {name}"
606                    )));
607                }
608                self.occupied()
609                    .filter(|(at, _)| at.profile == *profile && at.index + 1 == *slot)
610                    .map(|(_, occupant)| occupant.running_job())
611                    .collect()
612            }
613            api::CacheKind::Pnpm | api::CacheKind::Maven | api::CacheKind::Mirror => self
614                .occupied()
615                .map(|(_, occupant)| occupant.running_job())
616                .collect(),
617        };
618        if !users.is_empty() {
619            return Err(ControlError::Busy {
620                message: format!(
621                    "{name} is in use by {}; clean it once they finish",
622                    list(&users)
623                ),
624                jobs: users,
625            });
626        }
627        if volume.kind == api::CacheKind::Mirror && self.mirror_up {
628            cache::mirror_down(&*self.docker, &self.names)
629                .await
630                .map_err(|error| {
631                    ControlError::Failed(format!("cannot stop the mirror: {error}"))
632                })?;
633            self.mirror_up = false;
634        }
635        self.docker
636            .remove_volume(name)
637            .await
638            .map_err(|error| ControlError::Failed(format!("cannot remove {name}: {error}")))?;
639        info!("removed {name}");
640        // Measured again once idle.
641        self.trim_due = true;
642        Ok(())
643    }
644
645    /// Runs with a new, validated config. Running jobs keep running: a removed profile, or slots past a lowered
646    /// number, stay until their runners finish, and no runner starts in a profile while it has as many as its
647    /// slots. Runners started from now on get the new settings.
648    pub(super) fn reload(&mut self, config: Config, memory_max: Option<u64>) {
649        let mut old = std::mem::replace(&mut self.config, config);
650        for (name, profile) in &self.config.profiles {
651            let slots = self.slots.entry(name.clone()).or_default();
652            if slots.len() < profile.slots as usize {
653                slots.resize(profile.slots as usize, None);
654            }
655            self.retired.remove(name);
656        }
657        for (name, profile) in std::mem::take(&mut old.profiles) {
658            if !self.config.profiles.contains_key(&name) {
659                self.retired.insert(name, profile);
660            }
661        }
662        self.drop_closed_slots();
663        let names: BTreeSet<String> = self
664            .config
665            .repositories
666            .iter()
667            .map(|repository| repository.name.clone())
668            .collect();
669        let removed: Vec<String> = self
670            .repositories
671            .keys()
672            .filter(|name| !names.contains(*name))
673            .cloned()
674            .collect();
675        self.draining.extend(removed);
676        self.draining.retain(|name| !names.contains(name));
677        self.repositories.retain(|name, _| names.contains(name));
678        for name in &names {
679            self.repositories.entry(name.clone()).or_default();
680        }
681        self.control.set_repositories(names);
682        let profiles = &self.config.profiles;
683        self.reseed_requested
684            .retain(|name| profiles.contains_key(name));
685        self.isolation = isolation::backend(self.config.host.isolation);
686        self.memory_max = memory_max;
687        self.caches
688            .send_replace(cache::volumes(&self.names, &self.config));
689        info!("reloaded the config");
690    }
691}