1use std::future::Future;
4use std::sync::Arc;
5use std::time::Duration;
6use std::{fmt, io};
7
8use tokio::net::TcpListener;
9
10use crate::api::{self, App, Limits, TlsError};
11use crate::clock::{Clock, SystemClock};
12use crate::config::Config;
13use crate::datadir::{self, LockError, Work};
14use crate::envelope::{Key, KeyError};
15use crate::fs::OsFs;
16use crate::retention::Retention;
17use crate::tenants::{KeyOrder, RegistryError, RotationBacklog, TenantError, Tenants, with_causes};
18use time::OffsetDateTime;
19
20#[derive(Debug)]
22pub enum ServeError {
23 DataDir(LockError),
25 MasterKey(KeyError),
27 WrongMasterKey,
29 Open(RegistryError),
31 Registry(TenantError),
33 KeysOutOfOrder(KeyOrder),
35 Tls(TlsError),
37 Listen(io::Error),
39 Serve(io::Error),
41}
42
43impl fmt::Display for ServeError {
44 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
45 match self {
46 ServeError::DataDir(e) => write!(f, "{e}"),
47 ServeError::MasterKey(e) => write!(f, "the master key cannot be read: {e:?}"),
48 ServeError::WrongMasterKey => {
49 f.write_str("the master key is not the one server.sqlite was made under")
50 }
51 ServeError::Open(e) => {
52 write!(f, "the registry cannot be opened: {}", with_causes(e))
53 }
54 ServeError::Registry(e) => write!(f, "server.sqlite cannot be opened: {e:?}"),
55 ServeError::KeysOutOfOrder(order) => {
56 let mut parts = Vec::new();
57 if order.wrapped_elsewhere > 0 {
58 parts.push(format!(
59 "{} tenant key(s) do not unwrap under the master key",
60 order.wrapped_elsewhere
61 ));
62 }
63 if order.rotations_stopped > 0 {
64 parts.push(format!(
65 "{} key rotation(s) stopped part way; run rekey tenant again",
66 order.rotations_stopped
67 ));
68 }
69 if order.data_under_other_keys > 0 {
70 parts.push(format!(
71 "{} tenant(s) with data under another key",
72 order.data_under_other_keys
73 ));
74 }
75 if order.catalogs_unopened > 0 {
76 let why = order
77 .unopened_error
78 .as_ref()
79 .map(|e| format!(": {e}"))
80 .unwrap_or_default();
81 parts.push(format!(
82 "{} tenant catalog(s) cannot be opened{why}",
83 order.catalogs_unopened
84 ));
85 }
86 write!(
87 f,
88 "{}; vcon-server check names the tenants",
89 parts.join("; ")
90 )
91 }
92 ServeError::Tls(e) => write!(f, "the TLS certificate or key cannot be used: {e:?}"),
93 ServeError::Listen(e) => write!(f, "the listening address cannot be bound: {e}"),
94 ServeError::Serve(e) => write!(f, "serving failed: {e}"),
95 }
96 }
97}
98
99pub async fn run(config: &Config, shutdown: impl Future<Output = ()>) -> Result<(), ServeError> {
102 let _lock =
103 datadir::lock(&config.data_dir, Work::Serve.sharing()).map_err(ServeError::DataDir)?;
104 let master = Key::load_master(&config.master_key).map_err(ServeError::MasterKey)?;
105 let tenants = Tenants::open(
106 &config.data_dir,
107 master,
108 Arc::new(OsFs),
109 Arc::new(SystemClock),
110 )
111 .map_err(|e| match e {
112 RegistryError::WrongKey => ServeError::WrongMasterKey,
113 other => ServeError::Open(other),
114 })?;
115 let order = tenants.key_order().map_err(ServeError::Registry)?;
116 if !order.is_sound() {
117 return Err(ServeError::KeysOutOfOrder(order));
118 }
119 let tls = api::tls_config(&config.tls_cert, &config.tls_key).map_err(ServeError::Tls)?;
120 let listener = TcpListener::bind(config.listen)
121 .await
122 .map_err(ServeError::Listen)?;
123 let bound = listener.local_addr().map_err(ServeError::Listen)?;
126 let tenants = Arc::new(tenants);
127 let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
130 let (swept, default) = (tenants.clone(), config.retention_default);
131 let sweeper = tokio::spawn(every(
132 config.purge_interval,
133 async {
134 let _ = stopped.await;
135 },
136 move || {
137 let tenants = swept.clone();
138 async move {
139 let _ = tokio::task::spawn_blocking(move || sweep(&tenants, default)).await;
140 }
141 },
142 ));
143 let (stop_warning, warning_stopped) = tokio::sync::oneshot::channel::<()>();
145 let (watched, rotation_warn) = (tenants.clone(), config.rotation_warn);
146 let warner = tokio::spawn(every(
147 ROTATION_CHECK,
148 async {
149 let _ = warning_stopped.await;
150 },
151 move || {
152 let tenants = watched.clone();
153 async move {
154 let _ = tokio::task::spawn_blocking(move || {
155 warn_if_rotation_overdue(&tenants, rotation_warn);
156 })
157 .await;
158 }
159 },
160 ));
161 let app = app_from(config, tenants);
162 tracing::info!(listen = %bound, "serving");
163 let served = api::serve(listener, tls, api::router(Arc::new(app)), shutdown).await;
164 let _ = stop.send(());
165 let _ = stop_warning.send(());
166 let _ = sweeper.await;
167 let _ = warner.await;
168 served.map_err(ServeError::Serve)?;
169 tracing::info!("stopped");
170 Ok(())
171}
172
173#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
175pub struct SweepReport {
176 pub purged: u64,
178 pub failed: u64,
180}
181
182pub fn sweep(tenants: &Tenants, default: Retention) -> SweepReport {
187 let mut report = SweepReport::default();
188 match tenants.ids() {
189 Ok(ids) => {
190 for id in ids {
191 let purged = tenants.retention(id).and_then(|own| {
192 let store = tenants.store(id)?;
193 Ok(store.purge_due(crate::tenants::kept_for(own, default)))
194 });
195 match purged {
196 Ok(Ok(done)) => report.purged += done.purged,
197 _ => report.failed += 1,
198 }
199 }
200 }
201 Err(_) => report.failed += 1,
202 }
203 tracing::info!(
204 purged = report.purged,
205 failed = report.failed,
206 "purge sweep"
207 );
208 report
209}
210
211pub async fn every<F, Fut>(interval: Duration, shutdown: impl Future<Output = ()>, mut job: F)
214where
215 F: FnMut() -> Fut,
216 Fut: Future<Output = ()>,
217{
218 tokio::pin!(shutdown);
219 loop {
220 tokio::select! {
221 biased;
222 () = &mut shutdown => return,
223 () = job() => {}
224 }
225 tokio::select! {
226 biased;
227 () = &mut shutdown => return,
228 () = tokio::time::sleep(interval) => {}
229 }
230 }
231}
232
233fn app_from(config: &Config, tenants: Arc<Tenants>) -> App {
235 App {
236 tenants,
237 limits: Limits {
238 body: config.body_limit,
239 inflated: config.inflated_limit,
240 body_timeout: config.read_timeout,
241 },
242 supported: config.supported_critical.clone(),
243 retention_default: config.retention_default,
244 mcp: config.mcp,
245 }
246}
247
248pub fn rotation_overdue(
251 backlog: &RotationBacklog,
252 rotation_warn: Duration,
253 now: OffsetDateTime,
254) -> bool {
255 [backlog.oldest_purge, backlog.oldest_removal]
256 .into_iter()
257 .flatten()
258 .any(|at| now - at > rotation_warn)
259}
260
261const ROTATION_CHECK: Duration = Duration::from_secs(86_400);
263
264fn warn_if_rotation_overdue(tenants: &Tenants, rotation_warn: Duration) {
266 match tenants.rotation_backlog() {
267 Ok(backlog) if rotation_overdue(&backlog, rotation_warn, SystemClock.now()) => {
268 tracing::warn!(
269 tenants_with_purges = backlog.tenants_with_purges,
270 purges = backlog.purges,
271 removals = backlog.removals,
272 "key rotation overdue: purged vCons and removed tenants stay readable on old copies until it"
273 );
274 }
275 Ok(_) => {}
276 Err(_) => tracing::warn!("the key rotation backlog could not be read"),
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use std::time::Duration;
283
284 use super::*;
285
286 mod startup {
287 use time::macros::datetime;
288
289 use super::*;
290
291 const NOW: OffsetDateTime = datetime!(2026-10-25 12:00 UTC);
292 const DAY: Duration = Duration::from_secs(86_400);
293
294 #[test]
295 fn rotation_is_overdue_once_the_oldest_waiting_work_passes_rotation_warn() {
296 let backlog =
297 |purge: Option<OffsetDateTime>, removal: Option<OffsetDateTime>| RotationBacklog {
298 tenants_with_purges: u64::from(purge.is_some()),
299 purges: u64::from(purge.is_some()),
300 oldest_purge: purge,
301 removals: u64::from(removal.is_some()),
302 oldest_removal: removal,
303 };
304 let day_ago = Some(datetime!(2026-10-24 12:00 UTC));
305 let older = Some(datetime!(2026-10-24 11:59:59 UTC));
306 for (b, overdue) in [
307 (backlog(None, None), false),
308 (backlog(day_ago, None), false),
309 (backlog(older, None), true),
310 (backlog(None, older), true),
311 (backlog(day_ago, older), true),
312 ] {
313 assert_eq!(rotation_overdue(&b, DAY, NOW), overdue, "{b:?}");
314 }
315 }
316
317 #[test]
318 fn a_refusal_for_keys_out_of_order_counts_and_points_at_check() {
319 let order = KeyOrder {
320 wrapped_elsewhere: 1,
321 rotations_stopped: 2,
322 data_under_other_keys: 3,
323 ..KeyOrder::default()
324 };
325 assert_eq!(
326 ServeError::KeysOutOfOrder(order).to_string(),
327 "1 tenant key(s) do not unwrap under the master key; \
328 2 key rotation(s) stopped part way; run rekey tenant again; \
329 3 tenant(s) with data under another key; \
330 vcon-server check names the tenants",
331 "each count in its fixed place, then where to look"
332 );
333 }
334
335 #[test]
338 fn a_refusal_for_catalogs_it_may_not_open_gives_the_error_not_a_key() {
339 let order = KeyOrder {
340 catalogs_unopened: 2,
341 unopened_error: Some("Permission denied (os error 13)".to_owned()),
342 ..KeyOrder::default()
343 };
344 assert_eq!(
345 ServeError::KeysOutOfOrder(order).to_string(),
346 "2 tenant catalog(s) cannot be opened: Permission denied (os error 13); vcon-server check names the tenants"
347 );
348 }
349 }
350 use crate::retention::Retention;
351
352 #[test]
353 fn the_api_takes_its_limits_and_extensions_from_the_configuration() {
354 let dir = tempfile::tempdir().expect("temp dir");
355 let tenants = Arc::new(
356 Tenants::open(
357 dir.path(),
358 Key::generate().expect("key"),
359 Arc::new(OsFs),
360 Arc::new(SystemClock),
361 )
362 .expect("opens"),
363 );
364 let config = Config {
365 listen: "127.0.0.1:8443".parse().expect("address"),
366 tls_cert: "cert.pem".into(),
367 tls_key: "key.pem".into(),
368 data_dir: dir.path().to_owned(),
369 master_key: "master.key".into(),
370 retention_default: Retention::Months(18),
371 purge_interval: Duration::from_secs(3600),
372 rotation_warn: Duration::from_secs(86400),
373 body_limit: 12_345,
374 inflated_limit: 67_890,
375 read_timeout: Duration::from_secs(17),
376 supported_critical: ["known".to_owned()].into(),
377 mcp: crate::mcp::Limits {
378 calls_per_second: 3,
379 concurrent: 4,
380 response_bytes: 5,
381 },
382 };
383 let app = app_from(&config, tenants.clone());
384 assert!(Arc::ptr_eq(&app.tenants, &tenants));
385 assert_eq!(app.limits.body, 12_345);
386 assert_eq!(app.limits.inflated, 67_890);
387 assert_eq!(app.limits.body_timeout, Duration::from_secs(17));
388 assert_eq!(app.supported, config.supported_critical);
389 assert_eq!(app.retention_default, config.retention_default);
390 assert_eq!(app.mcp, config.mcp);
391 }
392
393 mod sweeps {
394 use std::sync::Arc;
395 use std::sync::atomic::{AtomicUsize, Ordering};
396 use std::time::Duration;
397
398 use time::macros::datetime;
399 use uuid::Uuid;
400
401 use crate::audit::Actor;
402 use crate::ids::{KeyId, TenantId};
403 use crate::retention::Retention;
404 use crate::serve::{SweepReport, every, sweep};
405 use crate::store::Caller;
406 use crate::store::testing::vcon;
407 use crate::tenants::{Tenants, fixture};
408
409 const CALLER: Caller = Caller { key: KeyId([7; 8]) };
410
411 fn deleted(tenants: &Tenants, id: TenantId, uuid: &str) -> Uuid {
414 let store = tenants.store(id).expect("its store");
415 let (found, body) = vcon(uuid, "kept");
416 store.create(&CALLER, &found, &body).expect("stored");
417 store
418 .delete(&CALLER, found.uuid, Retention::Years(7))
419 .expect("deleted");
420 found.uuid
421 }
422
423 fn kept(tenants: &Tenants, id: TenantId) -> u64 {
425 tenants
426 .store(id)
427 .expect("its store")
428 .counts()
429 .expect("counts")
430 .versions
431 }
432
433 #[test]
434 fn a_sweep_purges_what_is_due_in_each_tenant_under_its_own_period() {
435 let f = fixture();
436 let own = f
437 .tenants
438 .create(Actor::Cli, "own", Some(Retention::Days(1)))
439 .expect("made");
440 let default = f.tenants.create(Actor::Cli, "default", None).expect("made");
441 let long = f
442 .tenants
443 .create(Actor::Cli, "long", Some(Retention::Years(7)))
444 .expect("made");
445 deleted(&f.tenants, own, "0199e5c6-0d7a-7c11-8000-000000000001");
446 deleted(&f.tenants, default, "0199e5c6-0d7a-7c11-8000-000000000002");
447 deleted(&f.tenants, long, "0199e5c6-0d7a-7c11-8000-000000000003");
448 f.clock.set(datetime!(2026-10-25 13:00 UTC));
449
450 let report = sweep(&f.tenants, Retention::Days(1));
451
452 assert_eq!(
453 report,
454 SweepReport {
455 purged: 2,
456 failed: 0
457 }
458 );
459 assert_eq!(kept(&f.tenants, own), 0);
460 assert_eq!(kept(&f.tenants, default), 0);
461 assert_eq!(kept(&f.tenants, long), 1, "seven years have not passed");
462 }
463
464 #[test]
465 fn a_sweep_goes_on_past_a_tenant_it_cannot_sweep() {
466 let f = fixture();
467 let damaged = f.tenants.create(Actor::Cli, "damaged", None).expect("made");
468 let fine = f.tenants.create(Actor::Cli, "fine", None).expect("made");
469 deleted(&f.tenants, damaged, "0199e5c6-0d7a-7c11-8000-000000000001");
470 deleted(&f.tenants, fine, "0199e5c6-0d7a-7c11-8000-000000000002");
471 f.tenants
472 .conn_for_test()
473 .execute(
474 "UPDATE tenant SET retention = '7w' WHERE id = ?1",
475 [damaged.0],
476 )
477 .expect("damaged");
478 f.clock.set(datetime!(2026-10-25 13:00 UTC));
479
480 let report = sweep(&f.tenants, Retention::Days(1));
481
482 assert_eq!(
483 report,
484 SweepReport {
485 purged: 1,
486 failed: 1
487 }
488 );
489 assert_eq!(kept(&f.tenants, fine), 0);
490 assert_eq!(kept(&f.tenants, damaged), 1, "left for a later sweep");
491 }
492
493 #[tokio::test(start_paused = true)]
494 async fn every_runs_at_start_then_after_each_interval_until_shutdown() {
495 let runs = Arc::new(AtomicUsize::new(0));
496 let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
497 let counted = runs.clone();
498 let task = tokio::spawn(every(
499 Duration::from_secs(60),
500 async move {
501 let _ = stopped.await;
502 },
503 move || {
504 counted.fetch_add(1, Ordering::SeqCst);
505 async {}
506 },
507 ));
508 let settle = || async {
509 for _ in 0..10 {
510 tokio::task::yield_now().await;
511 }
512 };
513
514 settle().await;
515 assert_eq!(runs.load(Ordering::SeqCst), 1, "at start");
516 tokio::time::advance(Duration::from_secs(59)).await;
517 settle().await;
518 assert_eq!(runs.load(Ordering::SeqCst), 1, "not before the interval");
519 tokio::time::advance(Duration::from_secs(1)).await;
520 settle().await;
521 assert_eq!(runs.load(Ordering::SeqCst), 2, "after the interval");
522
523 stop.send(()).expect("still running");
524 task.await.expect("ends on shutdown");
525 tokio::time::advance(Duration::from_secs(600)).await;
526 assert_eq!(runs.load(Ordering::SeqCst), 2, "none after shutdown");
527 }
528 }
529}