Files
RustProxy/src/runtime/updates.rs
T
2026-10-17 20:34:13 +02:00

186 lines
7.5 KiB
Rust

impl App {
pub fn start_dns_acl_updates(self: &Arc<Self>) {
self.tunnels.spawn(crate::acl::dynamic::poll_loop(
Arc::downgrade(self), self.shutdown.clone(),
));
}
pub fn start_geoip_updates(self: &Arc<Self>) {
self.tunnels.spawn(crate::geoip::update_poll_loop(
Arc::downgrade(self),
self.shutdown.clone(),
));
}
pub fn start_update_check(self: &Arc<Self>, config: crate::update_check::Config) {
self.software_update.configure(&config);
self.tunnels.spawn(crate::update_check::poll_loop(
self.software_update.clone(),
config,
self.shutdown.clone(),
));
}
/// Drain detached writers after the listeners and their handlers have stopped.
pub async fn drain_updates(&self) {
let _all_slots = self.update_slots.clone().acquire_many_owned(8).await;
}
/// The detached task completes persistence + publication even if the HTTP caller disconnects.
pub async fn update(self: &Arc<Self>, update: Update) -> Result<config::Envelope, ApiError> {
self.update_as(update, "local".into()).await
}
pub async fn update_as(
self: &Arc<Self>,
update: Update,
actor: String,
) -> Result<config::Envelope, ApiError> {
self.update_as_action(update, actor, None).await
}
pub async fn restore_as(
self: &Arc<Self>,
update: Update,
actor: String,
source_revision: u64,
) -> Result<config::Envelope, ApiError> {
self.update_as_action(update, actor, Some(source_revision))
.await
}
async fn update_as_action(
self: &Arc<Self>,
update: Update,
actor: String,
source_revision: Option<u64>,
) -> Result<config::Envelope, ApiError> {
let slot = self.update_slots.clone().try_acquire_owned().map_err(|_| {
ApiError(
StatusCode::TOO_MANY_REQUESTS,
"configuration update queue is full".into(),
)
})?;
let expected_revision = update.expected_revision;
let revision = expected_revision
.checked_add(1)
.ok_or_else(|| ApiError(StatusCode::BAD_REQUEST, "revision overflow".into()))?;
let policy = self.policy.clone();
let defaults = self.startup_listeners;
let previous = self.config.load_full();
let previous_config = previous.envelope.config.clone();
let active_listeners = self.active_listeners.clone();
let observed = self.docker.snapshot().await;
let desired_vhost_count = update.config.vhosts.len();
let desired_config = update.config;
self.startup_locks
.validate(&desired_config, self.policy.admin_addr)
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?;
crate::tls::validate_certificate_removals(&previous_config, &desired_config)
.map_err(|error| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{error:#}")))?;
let (prepared, invalidation, slot) = tokio::task::spawn_blocking(move || {
let effective = crate::docker::effective_config(desired_config, &observed)?;
let mut prepared = config::compile_with_active(
effective,
revision,
&policy,
defaults,
Some(&previous),
&active_listeners,
)?;
prepared
.envelope
.config
.vhosts
.truncate(desired_vhost_count);
let invalidation = cache_invalidation(&previous_config, &prepared.envelope.config);
Ok::<_, anyhow::Error>((prepared, invalidation, slot))
})
.await
.map_err(|_| {
ApiError(
StatusCode::INTERNAL_SERVER_ERROR,
"configuration validation task failed".into(),
)
})?
.map_err(|e| ApiError(StatusCode::UNPROCESSABLE_ENTITY, format!("{e:#}")))?;
let this = self.clone();
tokio::spawn(async move {
let _slot = slot;
let _lock = this.update_lock.lock().await;
if this.config.load().envelope.revision != expected_revision {
return Err(ApiError(
StatusCode::CONFLICT,
"configuration changed; reload before saving".into(),
));
}
let persisted = match source_revision {
Some(source) => {
this.store
.replace_as_restore(
expected_revision,
&prepared.envelope.config,
&actor,
source,
)
.await
}
None => {
this.store
.replace_as(expected_revision, &prepared.envelope.config, &actor)
.await
}
};
match persisted {
Ok(true) => {}
Ok(false) => {
return Err(ApiError(
StatusCode::CONFLICT,
"stored revision differs; external SQLite writes are unsupported".into(),
))
}
Err(error) => {
tracing::error!(%error, "configuration persistence failed");
return Err(ApiError(
StatusCode::INTERNAL_SERVER_ERROR,
"configuration was not saved".into(),
));
}
}
// No await between successful commit and publication. Cache identity is
// intentionally decoupled from the configuration revision, so unrelated
// control-plane saves do not invalidate hot/cold objects.
let envelope = prepared.envelope.clone();
// After commit, reject late cache tickets from the old routing
// snapshot before any scope purge. Existing unaffected objects stay.
this.cache.set_control_revision(envelope.revision);
match invalidation {
CacheInvalidation::All(cache_revision) => {
this.cache.purge(Some(cache_revision));
}
CacheInvalidation::Scopes(scopes) => {
for scope in scopes {
this.cache.purge_scope(&scope);
}
}
CacheInvalidation::None => {}
}
// A COLD entry is usable after restart only if its scope still has
// the same origin/content policy. Publish the fingerprints after
// the cache generation barriers but before the routing snapshot.
this.cache.replace_scope_revisions(vhost_cache_revisions(&envelope.config));
this.cache.configure(envelope.config.cache.clone());
this.rate_limiter.reset();
this.logs.configure(envelope.config.logging.clone());
crate::logging::configure_error_log(envelope.config.error_log.clone());
this.config.store(Arc::new(prepared));
crate::acl::dynamic::config_changed();
tracing::info!(revision, "configuration applied");
Ok(envelope)
})
.await
.map_err(|_| {
ApiError(
StatusCode::INTERNAL_SERVER_ERROR,
"configuration task failed".into(),
)
})?
}
}