server/lib/src/live_routes.rs
//! **A repository made at run time serves at once** (lane M4b). The route tables are built
//! once at boot; a paid signup's repository (`add_principal`, lane M4) was served only from
//! the next start. [`LiveRoutes`] is what the boot keeps to make one more: the doors' route
//! tables (shared [`FastRoutes`] handles, so the gRPC door, the HTTP door and the job runner
//! of a door all see the new route), every repository's declaration, the handler catalog,
//! the growth's hold and (metered) the paid limit. [`LiveRoutes::serve`] builds the new
//! repository's backend exactly as the boot does — its own backend from its declaration,
//! wrapped in the growth's hold, then in the paid limit — and swaps it into its door's table.
//! A request in flight keeps the backend (and the table) it already resolved.
//!
//! What the boot alone can serve stays "served from the next start", said by name: a template
//! with upstreams (the pull-through proxy is wired over the boot's pointers) and a template
//! a credential gates (the read gate is fixed at boot; serving the copy open would be a leak).
use std::collections::BTreeMap;
use std::sync::Arc;
use crate::exposed::fast_routes::FastRoutes;
use crate::lock::RwLockOrRecover as _;
use crate::repository::Repository;
/// What the boot keeps to serve a repository made after it.
pub struct LiveRoutes {
/// Each exposed endpoint's route table, by the endpoint's name.
pub(crate) doors: Vec<(String, FastRoutes)>,
/// Every non-group repository's declaration (RON), by name.
pub(crate) specs: BTreeMap<String, String>,
pub(crate) catalog: Arc<crate::handlers::HandlerCatalog>,
pub(crate) hold: Arc<crate::grow::Hold>,
pub(crate) read_gate: Arc<crate::auth::ReadGate>,
/// The paid limit and the data root its guard reads (metered, a primary with a key).
#[cfg(feature = "metered")]
pub(crate) quota: Option<(Arc<crate::metered::Quota>, std::path::PathBuf)>,
/// **Tables that route every repository whatever its door** (lane P4): the replication
/// door's, which a certified twin follows and a recharge pulls through. A repository made
/// at run time is added to each, so the twin can follow it; one made before a table is
/// registered is added at registration ([`LiveRoutes::also_route`]).
pub(crate) every: std::sync::RwLock<Vec<FastRoutes>>,
/// Every repository made at run time, with the backend its door routes.
pub(crate) made: std::sync::RwLock<Vec<(String, Arc<dyn traits::RepositoryBackendTrait>)>>,
}
impl LiveRoutes {
/// True when some door serves `name` now.
pub fn serves(&self, name: &str) -> bool {
self.doors.iter().any(|(_, t)| t.lookup(name).is_some())
}
/// **Route every repository made at run time on `table` too, from now on** — the
/// replication door's (lane P4). Those made before are added at once.
pub fn also_route(&self, table: FastRoutes) {
let mut every = self.every.write_or_recover();
for (name, b) in self.made.read_or_recover().iter() {
table.add(name, b.clone());
}
every.push(table);
}
/// True when `name` is a repository this start declared (one a made repository may be
/// shaped like).
pub fn declares(&self, name: &str) -> bool {
self.specs.contains_key(name)
}
/// **`name` is owner-only from now on** (lane P1, [`crate::auth::ReadGate::own`]).
pub fn own(&self, name: &str, owner: &str) {
self.read_gate.own(name, owner);
}
/// [`LiveRoutes::own`] with the record's grants too (a twin following, a recharge).
pub fn own_entry(&self, name: &str, entry: crate::auth::SoldEntry) {
self.read_gate.own_entry(name, entry);
}
/// The backend some door serves `name` with now.
pub fn lookup(&self, name: &str) -> Option<Arc<dyn traits::RepositoryBackendTrait>> {
self.doors.iter().find_map(|(_, t)| t.lookup(name))
}
/// **The backend a made repository's bytes are written through** (lane P4: a twin that
/// follows a sale, a main recharged after one). Served at once when it can be
/// ([`LiveRoutes::serve`]); a repository only a start can serve (a gated or pull-through
/// template) gets a backend over its own bytes that no door routes yet, so a follow or a
/// recharge still holds every artifact and the next start serves them. `Err` only when no
/// backend can be made at all (its template is gone, its declaration makes none).
pub fn backend_for(
&self,
name: &str,
template: &str,
private: bool,
) -> Result<Arc<dyn traits::RepositoryBackendTrait>, String> {
if let Some(b) = self.lookup(name) {
return Ok(b);
}
let why = match self.serve_as(name, template, private) {
Ok(()) => {
return self
.lookup(name)
.ok_or_else(|| format!("'{name}' was served and no door routes it"))
}
Err(why) => why,
};
let spec = self.specs.get(template).ok_or_else(|| {
format!("its template '{template}' is not a repository this start declared")
})?;
let tmpl: Repository = ron::from_str(spec)
.map_err(|e| format!("its template '{template}' cannot be read again ({e})"))?;
let mut repo = crate::sold_copy(&tmpl, name)?;
repo.backend_from_config_with(&self.catalog)
.map_err(|e| format!("its backend cannot be made ({e:#})"))?;
let inner = repo
.backend_repository
.take()
.ok_or_else(|| "its declaration made no backend".to_string())?;
log::warn!(
"repository '{name}': its bytes are kept, and it is served from the next start ({why})"
);
let backend = crate::held_backend(&repo, inner, &self.hold, &self.catalog);
// No public door routes it, but the replication door does: a recharge pulls it back.
let every = self.every.read_or_recover();
for t in every.iter() {
t.add(name, backend.clone());
}
self.made
.write_or_recover()
.push((name.to_owned(), backend.clone()));
Ok(backend)
}
/// **Serve `name`, shaped like `template`, from now on** (idempotent). `Err` says why it
/// is served only from the next start; nothing was changed then.
pub fn serve(&self, name: &str, template: &str) -> Result<(), String> {
self.serve_as(name, template, false)
}
/// [`LiveRoutes::serve`], and with `private` its reads need a credential of this server
/// from the first request on (lane P4): the read gate learns the name BEFORE any door
/// routes it, so no read ever finds it open.
pub fn serve_as(&self, name: &str, template: &str, private: bool) -> Result<(), String> {
if self.serves(name) {
return Ok(());
}
let spec = self.specs.get(template).ok_or_else(|| {
format!("its template '{template}' is not a repository this start declared")
})?;
let tmpl: Repository = ron::from_str(spec)
.map_err(|e| format!("its template '{template}' cannot be read again ({e})"))?;
if self.read_gate.repo_requires_auth(template) || self.read_gate.floor(template).is_some() {
return Err(format!("its template '{template}' is gated by a credential, which the read gate learns at a start"));
}
if !tmpl.ron_upstreams.is_empty() {
return Err(format!(
"its template '{template}' pulls through upstreams, which are wired at a start"
));
}
let door = tmpl
.ron_out
.as_ref()
.map(|o| o.ron_exposed_endpoint.clone())
.ok_or_else(|| format!("its template '{template}' is exposed on no door"))?;
let table = self
.doors
.iter()
.find(|(n, _)| *n == door)
.map(|(_, t)| t)
.ok_or_else(|| format!("its door '{door}' has no route table"))?;
let mut repo = crate::sold_copy(&tmpl, name)?;
repo.ron_require_auth |= private;
repo.backend_from_config_with(&self.catalog)
.map_err(|e| format!("its backend cannot be made ({e:#})"))?;
let inner = repo
.backend_repository
.take()
.ok_or_else(|| "its declaration made no backend".to_string())?;
if private {
self.read_gate.gate_from_now(name);
}
#[allow(unused_mut)]
let mut backend = crate::held_backend(&repo, inner, &self.hold, &self.catalog);
#[cfg(feature = "metered")]
if let Some((quota, root)) = &self.quota {
quota.add_places(name, repo.storage_places());
backend = crate::metered::QuotaGuard::wrap(
backend,
name.to_string(),
Arc::clone(quota),
root.clone(),
);
}
{
// Under the `every` lock, so a table registered meanwhile misses nothing.
let every = self.every.read_or_recover();
table.add(name, backend.clone());
for t in every.iter() {
t.add(name, backend.clone());
}
self.made
.write_or_recover()
.push((name.to_owned(), backend));
}
log::warn!("repository '{name}': made at run time, shaped like '{template}', served now on door '{door}'");
Ok(())
}
}