Skip to main content

vcon_server/
serve.rs

1//! `vcon-server serve`: the server, run from its configuration.
2
3use 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/// Why the server did not start, or stopped.
22#[derive(Debug)]
23pub enum ServeError {
24    /// The data directory could not be taken.
25    DataDir(LockError),
26    /// The master key file could not be read.
27    MasterKey(KeyError),
28    /// server.sqlite was made under another master key.
29    WrongMasterKey,
30    /// The registry could not be opened.
31    Open(RegistryError),
32    /// server.sqlite could not be read.
33    Registry(TenantError),
34    /// The storage the configuration names cannot be used.
35    Storage(StorageError),
36    /// Some tenant's data is not under its current key.
37    KeysOutOfOrder(KeyOrder),
38    /// `tenants` tenants have packed versions, and the configuration has no
39    /// `[storage.parquet]`.
40    ParquetRemoved {
41        /// How many tenants have packed versions.
42        tenants: u64,
43    },
44    /// The certificate or key could not be used.
45    Tls(TlsError),
46    /// The listening address could not be bound.
47    Listen(io::Error),
48    /// Serving failed.
49    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
118/// Runs the server until `shutdown` completes: locks the data directory,
119/// opens the registry under the master key, and serves the API over TLS.
120pub 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    // Packed versions need Parquet storage kept on: nothing turns them
142    // back into objects, and only with it is the bucket checked for what
143    // would keep a purged row's bytes.
144    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    // The address bound, which names the port the system chose when the
157    // configuration gives port 0.
158    let bound = listener.local_addr().map_err(ServeError::Listen)?;
159    let tenants = Arc::new(tenants);
160    // The purge sweep: at start, then purge_interval after each sweep ends,
161    // until the server stops.
162    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    // The pack run, with Parquet storage: at start, then pack_interval
177    // after each run ends, until the server stops.
178    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    // The rotation warning: at start, then each day.
195    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/// What one pack run over every tenant did.
229#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
230pub struct PackRunReport {
231    /// Pack files written and recorded, over every tenant.
232    pub files: u64,
233    /// Versions moved into them.
234    pub versions: u64,
235    /// Tenants whose run failed; they wait for the next run.
236    pub failed: u64,
237}
238
239/// One pack run over every tenant, as `Store::pack` does for one, with the
240/// configured limit and reader grace. A tenant whose run fails is
241/// counted and left for the next run. Logged with counts only, never a
242/// tenant.
243pub 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/// What one purge sweep did.
273#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
274pub struct SweepReport {
275    /// Versions purged, over every tenant.
276    pub purged: u64,
277    /// Tenants that could not be swept; they wait for the next sweep.
278    pub failed: u64,
279}
280
281/// One purge sweep: each tenant purges what is due under its own retention
282/// period, else `default`. A tenant that cannot be swept, such as one whose
283/// period does not read back or that is being removed, is counted and left
284/// for the next sweep. The sweep is logged with counts only, never a tenant.
285pub 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
310/// Runs `job` at once, then again `interval` after each run ends, until
311/// `shutdown` completes; a run still going when it does is not awaited.
312pub 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
332/// What the API serves from, as the configuration sets it.
333fn 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
347/// Whether key rotation is overdue: the oldest purge or removal waiting
348/// for it is older than `rotation_warn`.
349pub 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
360/// How often `serve` looks at the rotation backlog.
361const ROTATION_CHECK: Duration = Duration::from_secs(86_400);
362
363/// Logs a warning, with counts only, when key rotation is overdue.
364fn 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        /// The refusal names the table that was taken out, says what goes
391        /// wrong without it, and where the tenants are named.
392        #[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        /// Catalogs the server may not open are counted apart from keys,
450        /// with the system's error, and never a tenant or a path.
451        #[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        /// A vCon stored and deleted in `id` while seven years apply, so
528        /// it waits hidden for a sweep.
529        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        /// How many versions `id` still keeps.
540        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}