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::backend;
12use crate::clock::{Clock, SystemClock};
13use crate::config::{Config, ParquetSettings, StorageError};
14use crate::datadir::{self, LockError, Work};
15use crate::envelope::{Key, KeyError};
16use crate::fs::OsFs;
17use crate::retention::Retention;
18use crate::tenants::{KeyOrder, RegistryError, RotationBacklog, TenantError, Tenants, with_causes};
19use time::OffsetDateTime;
20
21#[derive(Debug)]
23pub enum ServeError {
24 DataDir(LockError),
26 MasterKey(KeyError),
28 WrongMasterKey,
30 Open(RegistryError),
32 Registry(TenantError),
34 Storage(StorageError),
36 KeysOutOfOrder(KeyOrder),
38 ParquetRemoved {
41 tenants: u64,
43 },
44 Tls(TlsError),
46 Listen(io::Error),
48 Serve(io::Error),
50}
51
52impl fmt::Display for ServeError {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 match self {
55 ServeError::DataDir(e) => write!(f, "{e}"),
56 ServeError::MasterKey(e) => write!(f, "the master key cannot be read: {e:?}"),
57 ServeError::WrongMasterKey => {
58 f.write_str("the master key is not the one server.sqlite was made under")
59 }
60 ServeError::Open(e) => {
61 write!(f, "the registry cannot be opened: {}", with_causes(e))
62 }
63 ServeError::Registry(e) => write!(f, "server.sqlite cannot be opened: {e:?}"),
64 ServeError::KeysOutOfOrder(order) => {
65 let mut parts = Vec::new();
66 if order.wrapped_elsewhere > 0 {
67 parts.push(format!(
68 "{} tenant key(s) do not unwrap under the master key",
69 order.wrapped_elsewhere
70 ));
71 }
72 if order.rotations_stopped > 0 {
73 parts.push(format!(
74 "{} key rotation(s) stopped part way; run rekey tenant again",
75 order.rotations_stopped
76 ));
77 }
78 if order.data_under_other_keys > 0 {
79 parts.push(format!(
80 "{} tenant(s) with data under another key",
81 order.data_under_other_keys
82 ));
83 }
84 if order.catalogs_unopened > 0 {
85 let why = order
86 .unopened_error
87 .as_ref()
88 .map(|e| format!(": {e}"))
89 .unwrap_or_default();
90 parts.push(format!(
91 "{} tenant catalog(s) cannot be opened{why}",
92 order.catalogs_unopened
93 ));
94 }
95 write!(
96 f,
97 "{}; vcon-server check names the tenants",
98 parts.join("; ")
99 )
100 }
101 ServeError::ParquetRemoved { tenants } => write!(
102 f,
103 "{tenants} tenant(s) have versions packed into Parquet files, and the \
104 configuration has no [storage.parquet]: without it the server packs no more, \
105 and the start no longer refuses a bucket with Object Lock or versioning, either \
106 of which keeps the bytes of a pack file that a purge rewrote; put the \
107 [storage.parquet] table back, since packed versions cannot be turned back into \
108 objects; vcon-server check names the tenants"
109 ),
110 ServeError::Tls(e) => write!(f, "the TLS certificate or key cannot be used: {e:?}"),
111 ServeError::Listen(e) => write!(f, "the listening address cannot be bound: {e}"),
112 ServeError::Serve(e) => write!(f, "serving failed: {e}"),
113 ServeError::Storage(e) => write!(f, "the storage cannot be used: {e}"),
114 }
115 }
116}
117
118pub async fn run(config: &Config, shutdown: impl Future<Output = ()>) -> Result<(), ServeError> {
121 let _lock =
122 datadir::lock(&config.data_dir, Work::Serve.sharing()).map_err(ServeError::DataDir)?;
123 let master = Key::load_master(&config.master_key).map_err(ServeError::MasterKey)?;
124 let objects =
125 backend::objects_for(&config.storage, &config.data_dir).map_err(ServeError::Storage)?;
126 let tenants = Tenants::open_in(
127 &config.data_dir,
128 master,
129 Arc::new(OsFs),
130 Arc::new(SystemClock),
131 objects,
132 )
133 .map_err(|e| match e {
134 RegistryError::WrongKey => ServeError::WrongMasterKey,
135 other => ServeError::Open(other),
136 })?;
137 let order = tenants.key_order().map_err(ServeError::Registry)?;
138 if !order.is_sound() {
139 return Err(ServeError::KeysOutOfOrder(order));
140 }
141 if config.storage.parquet.is_none() {
145 let tenants = tenants
146 .tenants_with_packed_versions()
147 .map_err(ServeError::Registry)?;
148 if tenants > 0 {
149 return Err(ServeError::ParquetRemoved { tenants });
150 }
151 }
152 let tls = api::tls_config(&config.tls_cert, &config.tls_key).map_err(ServeError::Tls)?;
153 let listener = TcpListener::bind(config.listen)
154 .await
155 .map_err(ServeError::Listen)?;
156 let bound = listener.local_addr().map_err(ServeError::Listen)?;
159 let tenants = Arc::new(tenants);
160 let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
163 let (swept, default) = (tenants.clone(), config.retention_default);
164 let sweeper = tokio::spawn(every(
165 config.purge_interval,
166 async {
167 let _ = stopped.await;
168 },
169 move || {
170 let tenants = swept.clone();
171 async move {
172 let _ = tokio::task::spawn_blocking(move || sweep(&tenants, default)).await;
173 }
174 },
175 ));
176 let (stop_packing, packing_stopped) = tokio::sync::oneshot::channel::<()>();
179 let packer = config.storage.parquet.map(|parquet| {
180 let packed = tenants.clone();
181 tokio::spawn(every(
182 parquet.pack_interval,
183 async {
184 let _ = packing_stopped.await;
185 },
186 move || {
187 let tenants = packed.clone();
188 async move {
189 let _ = tokio::task::spawn_blocking(move || pack_all(&tenants, &parquet)).await;
190 }
191 },
192 ))
193 });
194 let (stop_warning, warning_stopped) = tokio::sync::oneshot::channel::<()>();
196 let (watched, rotation_warn) = (tenants.clone(), config.rotation_warn);
197 let warner = tokio::spawn(every(
198 ROTATION_CHECK,
199 async {
200 let _ = warning_stopped.await;
201 },
202 move || {
203 let tenants = watched.clone();
204 async move {
205 let _ = tokio::task::spawn_blocking(move || {
206 warn_if_rotation_overdue(&tenants, rotation_warn);
207 })
208 .await;
209 }
210 },
211 ));
212 let app = app_from(config, tenants);
213 tracing::info!(listen = %bound, "serving");
214 let served = api::serve(listener, tls, api::router(Arc::new(app)), shutdown).await;
215 let _ = stop.send(());
216 let _ = stop_warning.send(());
217 let _ = stop_packing.send(());
218 let _ = sweeper.await;
219 let _ = warner.await;
220 if let Some(packer) = packer {
221 let _ = packer.await;
222 }
223 served.map_err(ServeError::Serve)?;
224 tracing::info!("stopped");
225 Ok(())
226}
227
228#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
230pub struct PackRunReport {
231 pub files: u64,
233 pub versions: u64,
235 pub failed: u64,
237}
238
239pub fn pack_all(tenants: &Tenants, settings: &ParquetSettings) -> PackRunReport {
244 let mut report = PackRunReport::default();
245 let plan = crate::store::packer::PackPlan {
246 grace: settings.reader_grace,
247 ..crate::store::packer::PackPlan::with_limit(settings.pack_limit)
248 };
249 match tenants.ids() {
250 Ok(ids) => {
251 for id in ids {
252 match tenants.store(id).map(|store| store.pack(&plan)) {
253 Ok(Ok(done)) => {
254 report.files += done.files;
255 report.versions += done.versions;
256 }
257 _ => report.failed += 1,
258 }
259 }
260 }
261 Err(_) => report.failed += 1,
262 }
263 tracing::info!(
264 files = report.files,
265 versions = report.versions,
266 failed = report.failed,
267 "pack run"
268 );
269 report
270}
271
272#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
274pub struct SweepReport {
275 pub purged: u64,
277 pub failed: u64,
279}
280
281pub fn sweep(tenants: &Tenants, default: Retention) -> SweepReport {
286 let mut report = SweepReport::default();
287 match tenants.ids() {
288 Ok(ids) => {
289 for id in ids {
290 let purged = tenants.retention(id).and_then(|own| {
291 let store = tenants.store(id)?;
292 Ok(store.purge_due(crate::tenants::kept_for(own, default)))
293 });
294 match purged {
295 Ok(Ok(done)) => report.purged += done.purged,
296 _ => report.failed += 1,
297 }
298 }
299 }
300 Err(_) => report.failed += 1,
301 }
302 tracing::info!(
303 purged = report.purged,
304 failed = report.failed,
305 "purge sweep"
306 );
307 report
308}
309
310pub async fn every<F, Fut>(interval: Duration, shutdown: impl Future<Output = ()>, mut job: F)
313where
314 F: FnMut() -> Fut,
315 Fut: Future<Output = ()>,
316{
317 tokio::pin!(shutdown);
318 loop {
319 tokio::select! {
320 biased;
321 () = &mut shutdown => return,
322 () = job() => {}
323 }
324 tokio::select! {
325 biased;
326 () = &mut shutdown => return,
327 () = tokio::time::sleep(interval) => {}
328 }
329 }
330}
331
332fn app_from(config: &Config, tenants: Arc<Tenants>) -> App {
334 App {
335 tenants,
336 limits: Limits {
337 body: config.body_limit,
338 inflated: config.inflated_limit,
339 body_timeout: config.read_timeout,
340 },
341 supported: config.supported_critical.clone(),
342 retention_default: config.retention_default,
343 mcp: config.mcp,
344 }
345}
346
347pub fn rotation_overdue(
350 backlog: &RotationBacklog,
351 rotation_warn: Duration,
352 now: OffsetDateTime,
353) -> bool {
354 [backlog.oldest_purge, backlog.oldest_removal]
355 .into_iter()
356 .flatten()
357 .any(|at| now - at > rotation_warn)
358}
359
360const ROTATION_CHECK: Duration = Duration::from_secs(86_400);
362
363fn warn_if_rotation_overdue(tenants: &Tenants, rotation_warn: Duration) {
365 match tenants.rotation_backlog() {
366 Ok(backlog) if rotation_overdue(&backlog, rotation_warn, SystemClock.now()) => {
367 tracing::warn!(
368 tenants_with_purges = backlog.tenants_with_purges,
369 purges = backlog.purges,
370 removals = backlog.removals,
371 "key rotation overdue: purged vCons and removed tenants stay readable on old copies until it"
372 );
373 }
374 Ok(_) => {}
375 Err(_) => tracing::warn!("the key rotation backlog could not be read"),
376 }
377}
378
379#[cfg(test)]
380mod tests {
381 use std::time::Duration;
382
383 use super::*;
384
385 mod startup {
386 use time::macros::datetime;
387
388 use super::*;
389
390 #[test]
393 fn a_start_without_storage_parquet_over_packed_versions_names_the_table() {
394 assert_eq!(
395 ServeError::ParquetRemoved { tenants: 2 }.to_string(),
396 "2 tenant(s) have versions packed into Parquet files, and the configuration has no \
397 [storage.parquet]: without it the server packs no more, and the start no longer \
398 refuses a bucket with Object Lock or versioning, either of which keeps the bytes of \
399 a pack file that a purge rewrote; put the [storage.parquet] table back, since packed \
400 versions cannot be turned back into objects; vcon-server check names the tenants",
401 "the refusal's text"
402 );
403 }
404
405 const NOW: OffsetDateTime = datetime!(2026-10-25 12:00 UTC);
406 const DAY: Duration = Duration::from_secs(86_400);
407
408 #[test]
409 fn rotation_is_overdue_once_the_oldest_waiting_work_passes_rotation_warn() {
410 let backlog =
411 |purge: Option<OffsetDateTime>, removal: Option<OffsetDateTime>| RotationBacklog {
412 tenants_with_purges: u64::from(purge.is_some()),
413 purges: u64::from(purge.is_some()),
414 oldest_purge: purge,
415 removals: u64::from(removal.is_some()),
416 oldest_removal: removal,
417 };
418 let day_ago = Some(datetime!(2026-10-24 12:00 UTC));
419 let older = Some(datetime!(2026-10-24 11:59:59 UTC));
420 for (b, overdue) in [
421 (backlog(None, None), false),
422 (backlog(day_ago, None), false),
423 (backlog(older, None), true),
424 (backlog(None, older), true),
425 (backlog(day_ago, older), true),
426 ] {
427 assert_eq!(rotation_overdue(&b, DAY, NOW), overdue, "{b:?}");
428 }
429 }
430
431 #[test]
432 fn a_refusal_for_keys_out_of_order_counts_and_points_at_check() {
433 let order = KeyOrder {
434 wrapped_elsewhere: 1,
435 rotations_stopped: 2,
436 data_under_other_keys: 3,
437 ..KeyOrder::default()
438 };
439 assert_eq!(
440 ServeError::KeysOutOfOrder(order).to_string(),
441 "1 tenant key(s) do not unwrap under the master key; \
442 2 key rotation(s) stopped part way; run rekey tenant again; \
443 3 tenant(s) with data under another key; \
444 vcon-server check names the tenants",
445 "each count in its fixed place, then where to look"
446 );
447 }
448
449 #[test]
452 fn a_refusal_for_catalogs_it_may_not_open_gives_the_error_not_a_key() {
453 let order = KeyOrder {
454 catalogs_unopened: 2,
455 unopened_error: Some("Permission denied (os error 13)".to_owned()),
456 ..KeyOrder::default()
457 };
458 assert_eq!(
459 ServeError::KeysOutOfOrder(order).to_string(),
460 "2 tenant catalog(s) cannot be opened: Permission denied (os error 13); vcon-server check names the tenants"
461 );
462 }
463 }
464 use crate::retention::Retention;
465
466 #[test]
467 fn the_api_takes_its_limits_and_extensions_from_the_configuration() {
468 let dir = tempfile::tempdir().expect("temp dir");
469 let tenants = Arc::new(
470 Tenants::open(
471 dir.path(),
472 Key::generate().expect("key"),
473 Arc::new(OsFs),
474 Arc::new(SystemClock),
475 )
476 .expect("opens"),
477 );
478 let config = Config {
479 listen: "127.0.0.1:8443".parse().expect("address"),
480 tls_cert: "cert.pem".into(),
481 tls_key: "key.pem".into(),
482 data_dir: dir.path().to_owned(),
483 master_key: "master.key".into(),
484 retention_default: Retention::Months(18),
485 purge_interval: Duration::from_secs(3600),
486 rotation_warn: Duration::from_secs(86400),
487 body_limit: 12_345,
488 inflated_limit: 67_890,
489 read_timeout: Duration::from_secs(17),
490 supported_critical: ["known".to_owned()].into(),
491 mcp: crate::mcp::Limits {
492 calls_per_second: 3,
493 concurrent: 4,
494 response_bytes: 5,
495 },
496 storage: crate::config::Storage::default(),
497 export: crate::config::Export::default(),
498 };
499 let app = app_from(&config, tenants.clone());
500 assert!(Arc::ptr_eq(&app.tenants, &tenants));
501 assert_eq!(app.limits.body, 12_345);
502 assert_eq!(app.limits.inflated, 67_890);
503 assert_eq!(app.limits.body_timeout, Duration::from_secs(17));
504 assert_eq!(app.supported, config.supported_critical);
505 assert_eq!(app.retention_default, config.retention_default);
506 assert_eq!(app.mcp, config.mcp);
507 }
508
509 mod sweeps {
510 use std::sync::Arc;
511 use std::sync::atomic::{AtomicUsize, Ordering};
512 use std::time::Duration;
513
514 use time::macros::datetime;
515 use uuid::Uuid;
516
517 use crate::audit::Actor;
518 use crate::ids::{KeyId, TenantId};
519 use crate::retention::Retention;
520 use crate::serve::{SweepReport, every, sweep};
521 use crate::store::Caller;
522 use crate::store::testing::vcon;
523 use crate::tenants::{Tenants, fixture};
524
525 const CALLER: Caller = Caller { key: KeyId([7; 8]) };
526
527 fn deleted(tenants: &Tenants, id: TenantId, uuid: &str) -> Uuid {
530 let store = tenants.store(id).expect("its store");
531 let (found, body) = vcon(uuid, "kept");
532 store.create(&CALLER, &found, &body).expect("stored");
533 store
534 .delete(&CALLER, found.uuid, Retention::Years(7))
535 .expect("deleted");
536 found.uuid
537 }
538
539 fn kept(tenants: &Tenants, id: TenantId) -> u64 {
541 tenants
542 .store(id)
543 .expect("its store")
544 .counts()
545 .expect("counts")
546 .versions
547 }
548
549 #[test]
550 fn a_sweep_purges_what_is_due_in_each_tenant_under_its_own_period() {
551 let f = fixture();
552 let own = f
553 .tenants
554 .create(Actor::Cli, "own", Some(Retention::Days(1)))
555 .expect("made");
556 let default = f.tenants.create(Actor::Cli, "default", None).expect("made");
557 let long = f
558 .tenants
559 .create(Actor::Cli, "long", Some(Retention::Years(7)))
560 .expect("made");
561 deleted(&f.tenants, own, "0199e5c6-0d7a-7c11-8000-000000000001");
562 deleted(&f.tenants, default, "0199e5c6-0d7a-7c11-8000-000000000002");
563 deleted(&f.tenants, long, "0199e5c6-0d7a-7c11-8000-000000000003");
564 f.clock.set(datetime!(2026-10-25 13:00 UTC));
565
566 let report = sweep(&f.tenants, Retention::Days(1));
567
568 assert_eq!(
569 report,
570 SweepReport {
571 purged: 2,
572 failed: 0
573 }
574 );
575 assert_eq!(kept(&f.tenants, own), 0);
576 assert_eq!(kept(&f.tenants, default), 0);
577 assert_eq!(kept(&f.tenants, long), 1, "seven years have not passed");
578 }
579
580 #[test]
581 fn a_sweep_goes_on_past_a_tenant_it_cannot_sweep() {
582 let f = fixture();
583 let damaged = f.tenants.create(Actor::Cli, "damaged", None).expect("made");
584 let fine = f.tenants.create(Actor::Cli, "fine", None).expect("made");
585 deleted(&f.tenants, damaged, "0199e5c6-0d7a-7c11-8000-000000000001");
586 deleted(&f.tenants, fine, "0199e5c6-0d7a-7c11-8000-000000000002");
587 f.tenants
588 .conn_for_test()
589 .execute(
590 "UPDATE tenant SET retention = '7w' WHERE id = ?1",
591 [damaged.0],
592 )
593 .expect("damaged");
594 f.clock.set(datetime!(2026-10-25 13:00 UTC));
595
596 let report = sweep(&f.tenants, Retention::Days(1));
597
598 assert_eq!(
599 report,
600 SweepReport {
601 purged: 1,
602 failed: 1
603 }
604 );
605 assert_eq!(kept(&f.tenants, fine), 0);
606 assert_eq!(kept(&f.tenants, damaged), 1, "left for a later sweep");
607 }
608
609 #[tokio::test(start_paused = true)]
610 async fn every_runs_at_start_then_after_each_interval_until_shutdown() {
611 let runs = Arc::new(AtomicUsize::new(0));
612 let (stop, stopped) = tokio::sync::oneshot::channel::<()>();
613 let counted = runs.clone();
614 let task = tokio::spawn(every(
615 Duration::from_secs(60),
616 async move {
617 let _ = stopped.await;
618 },
619 move || {
620 counted.fetch_add(1, Ordering::SeqCst);
621 async {}
622 },
623 ));
624 let settle = || async {
625 for _ in 0..10 {
626 tokio::task::yield_now().await;
627 }
628 };
629
630 settle().await;
631 assert_eq!(runs.load(Ordering::SeqCst), 1, "at start");
632 tokio::time::advance(Duration::from_secs(59)).await;
633 settle().await;
634 assert_eq!(runs.load(Ordering::SeqCst), 1, "not before the interval");
635 tokio::time::advance(Duration::from_secs(1)).await;
636 settle().await;
637 assert_eq!(runs.load(Ordering::SeqCst), 2, "after the interval");
638
639 stop.send(()).expect("still running");
640 task.await.expect("ends on shutdown");
641 tokio::time::advance(Duration::from_secs(600)).await;
642 assert_eq!(runs.load(Ordering::SeqCst), 2, "none after shutdown");
643 }
644 }
645}