#[cfg(test)]
mod tests;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use crate::catalog::{Catalog, CatalogHandle, OnBroken};
use crate::config::{Config, Secret};
use crate::error::{CatalogError, ConfigError};
use crate::generation::Generation;
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[non_exhaustive]
pub(crate) struct Reload {
pub(crate) ranking_changed: bool,
pub(crate) retrieval_stale: bool,
pub(crate) published: bool,
}
#[derive(Debug)]
pub(crate) struct ReloadError {
repr: ReloadErrorRepr,
}
#[derive(Debug)]
enum ReloadErrorRepr {
Config(ConfigError),
Catalog(CatalogError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "the watcher acts on a reload failure through Display and source; the classifier exists for the tests and for a future caller that must branch on the class"
)
)]
pub(crate) enum ReloadErrorKind {
Config,
Catalog,
}
impl ReloadError {
fn config(source: ConfigError) -> ReloadError {
ReloadError {
repr: ReloadErrorRepr::Config(source),
}
}
fn catalog(source: CatalogError) -> ReloadError {
ReloadError {
repr: ReloadErrorRepr::Catalog(source),
}
}
#[must_use]
#[cfg_attr(
not(test),
expect(
dead_code,
reason = "paired with ReloadErrorKind: the classifier the tests use and a future caller branches on"
)
)]
pub(crate) fn kind(&self) -> ReloadErrorKind {
match &self.repr {
ReloadErrorRepr::Config(_) => ReloadErrorKind::Config,
ReloadErrorRepr::Catalog(_) => ReloadErrorKind::Catalog,
}
}
}
impl std::fmt::Display for ReloadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.repr {
ReloadErrorRepr::Config(_) => {
f.write_str("reload keeps the previous catalog: the configuration would not load")
}
ReloadErrorRepr::Catalog(_) => {
f.write_str("reload keeps the previous catalog: the candidate would not resolve")
}
}
}
}
impl std::error::Error for ReloadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match &self.repr {
ReloadErrorRepr::Config(source) => Some(source),
ReloadErrorRepr::Catalog(source) => Some(source),
}
}
}
#[derive(Debug)]
pub(crate) struct Reloader {
source: PathBuf,
boot: Arc<Config>,
catalog: Arc<CatalogHandle>,
cancel: Arc<AtomicBool>,
}
struct Pending {
ticket: u64,
generation: Generation,
outcome: Reload,
}
impl Reloader {
#[must_use]
pub(crate) fn new(source: &Path, boot: Arc<Config>, catalog: Arc<CatalogHandle>) -> Reloader {
Reloader {
source: source.to_path_buf(),
boot,
catalog,
cancel: Arc::new(AtomicBool::new(false)),
}
}
#[must_use]
pub(crate) fn cancel_handle(&self) -> Arc<AtomicBool> {
Arc::clone(&self.cancel)
}
pub(crate) fn reload(&self) -> Result<Reload, ReloadError> {
let pending = self.build()?;
Ok(self.commit(pending))
}
fn build(&self) -> Result<Pending, ReloadError> {
let ticket = self.catalog.claim();
let config = self.candidate_config()?;
let candidate =
Catalog::resolve(&config, OnBroken::Retain).map_err(ReloadError::catalog)?;
let previous = self.catalog.load();
let ranking_changed = previous.catalog().hash() != candidate.hash();
let broken = candidate
.entries()
.iter()
.filter(|entry| entry.problem().is_some())
.count();
let (retrieval, retrieval_stale) = if ranking_changed {
let reindex = previous.retrieval().rebuilt(&candidate);
let stale = reindex.is_stale();
(reindex.into_retrieval(), stale)
} else {
(previous.retrieval().clone(), false)
};
tracing::info!(
"reloaded {} prompt(s), {broken} broken; ranking {}, retrieval {}",
candidate.len(),
if ranking_changed {
"changed"
} else {
"unchanged"
},
if retrieval_stale { "stale" } else { "current" },
);
Ok(Pending {
ticket,
generation: Generation::new(candidate, retrieval),
outcome: Reload {
ranking_changed,
retrieval_stale,
published: false,
},
})
}
fn commit(&self, pending: Pending) -> Reload {
let Pending {
ticket,
generation,
mut outcome,
} = pending;
outcome.published = self.catalog.publish(&self.cancel, ticket, generation);
outcome
}
fn candidate_config(&self) -> Result<Config, ReloadError> {
let mut config = Config::load(&self.source).map_err(ReloadError::config)?;
let ignored = ignored_changes(&self.boot, &config);
if !ignored.is_empty() {
tracing::info!(
"{} changed and does not reload; restart to apply it: {}",
self.source.display(),
ignored.join(", ")
);
}
config.paths.prompts.clone_from(&self.boot.paths.prompts);
Ok(config)
}
}
pub(super) fn ignored_changes(boot: &Config, candidate: &Config) -> Vec<&'static str> {
let mut ignored = Vec::new();
if boot.server.bind != candidate.server.bind {
ignored.push("[server].bind");
}
if boot.server.token.as_ref().map(Secret::expose)
!= candidate.server.token.as_ref().map(Secret::expose)
{
ignored.push("[server].token");
}
if boot.server.max_concurrent_runs != candidate.server.max_concurrent_runs {
ignored.push("[server].max_concurrent_runs");
}
if boot.server.admission_timeout != candidate.server.admission_timeout {
ignored.push("[server].admission_timeout");
}
if boot.server.reply_deadline != candidate.server.reply_deadline {
ignored.push("[server].reply_deadline");
}
if boot.server.retain_completed != candidate.server.retain_completed {
ignored.push("[server].retain_completed");
}
if boot.server.watch != candidate.server.watch {
ignored.push("[server].watch");
}
if boot.server.watch_debounce != candidate.server.watch_debounce {
ignored.push("[server].watch_debounce");
}
if boot.server.allowed_hosts != candidate.server.allowed_hosts {
ignored.push("[server].allowed_hosts");
}
if boot.paths.prompts != candidate.paths.prompts {
ignored.push("[paths].prompts");
}
if boot.gateway.url != candidate.gateway.url {
ignored.push("[gateway].url");
}
if boot.gateway.key.expose() != candidate.gateway.key.expose() {
ignored.push("[gateway].key");
}
ignored
}