Skip to main content

sleet/
api.rs

1//! Supported programmatic API for controlling a Sleet fleet.
2
3use std::collections::HashSet;
4
5use tokio_util::sync::CancellationToken;
6
7use crate::config::Service;
8use crate::daemon::NodeOptions;
9use crate::mirror::RestorePoint;
10use crate::response::{
11    MirrorRestoreResponse, MirrorSyncResponse, RegisterResponse, StatusResponse,
12};
13use crate::root::FleetRoot;
14use crate::{daemon, ops};
15
16/// A fleet root opened with object-store settings from the process
17/// environment.
18#[derive(Clone)]
19pub struct Fleet {
20    root: FleetRoot,
21}
22
23impl Fleet {
24    /// Open a fleet root URL. Object-store credentials and provider
25    /// options come from the process environment.
26    pub fn open(url: &str) -> Result<Self, Error> {
27        Ok(Self {
28            root: FleetRoot::open(url)?,
29        })
30    }
31
32    /// Construct a facade over an existing root. This is an unstable
33    /// seam for tests and specialized embedding.
34    #[doc(hidden)]
35    pub fn from_root(root: FleetRoot) -> Self {
36        Self { root }
37    }
38
39    /// The canonical fleet root URL.
40    pub fn url(&self) -> &str {
41        self.root.url()
42    }
43
44    /// Register a database with a create-only registry write.
45    pub async fn register(&self, database_url: &str) -> Result<RegisterResponse, Error> {
46        Ok(ops::register(&self.root, database_url).await?)
47    }
48
49    /// Derive fleet status from object storage.
50    pub async fn status(&self, options: StatusOptions) -> Result<StatusResponse, Error> {
51        Ok(ops::status(&self.root, options.compactions, options.mirrors).await?)
52    }
53
54    /// Run one mirror sync pass for a registered database and target.
55    pub async fn sync_mirror(
56        &self,
57        database_url: &str,
58        target: &str,
59        options: MirrorSyncOptions,
60    ) -> Result<MirrorSyncResponse, Error> {
61        Ok(ops::mirror_sync(&self.root, database_url, target, options.rclone.as_deref()).await?)
62    }
63
64    /// Run a fleet node until `shutdown` is cancelled. The caller owns
65    /// the Tokio runtime, signal handling, and tracing subscriber.
66    pub async fn run_node(
67        &self,
68        options: NodeOptions,
69        shutdown: CancellationToken,
70    ) -> Result<(), Error> {
71        let mut seen = HashSet::new();
72        if let Some(duplicate) = options
73            .services
74            .iter()
75            .find(|service| !seen.insert(**service))
76        {
77            return Err(Error::DuplicateService {
78                service: *duplicate,
79            });
80        }
81        daemon::run(self.root.clone(), options, shutdown).await?;
82        Ok(())
83    }
84}
85
86/// Restore a backup URL into an empty destination URL.
87pub async fn mirror_restore(
88    backup_url: &str,
89    destination_url: &str,
90    point: RestorePoint,
91) -> Result<MirrorRestoreResponse, Error> {
92    Ok(ops::mirror_restore(backup_url, destination_url, point).await?)
93}
94
95/// Optional, more expensive status probes.
96#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
97pub struct StatusOptions {
98    compactions: bool,
99    mirrors: bool,
100}
101
102impl StatusOptions {
103    /// Include each database's compaction queue depth.
104    pub fn with_compactions(mut self, enabled: bool) -> Self {
105        self.compactions = enabled;
106        self
107    }
108
109    /// Include mirror source and destination lag.
110    pub fn with_mirrors(mut self, enabled: bool) -> Self {
111        self.mirrors = enabled;
112        self
113    }
114}
115
116/// Node-local options for a one-shot mirror sync.
117#[derive(Clone, Debug, Default, PartialEq, Eq)]
118pub struct MirrorSyncOptions {
119    rclone: Option<String>,
120}
121
122impl MirrorSyncOptions {
123    /// Set the rclone binary used by a target configured with the
124    /// rclone copier.
125    pub fn with_rclone(mut self, rclone: impl Into<String>) -> Self {
126        self.rclone = Some(rclone.into());
127        self
128    }
129}
130
131/// A supported API operation failure.
132#[derive(Debug, thiserror::Error)]
133#[non_exhaustive]
134pub enum Error {
135    /// The fleet root URL was rejected.
136    #[error("invalid fleet root: {0}")]
137    FleetRootUrl(#[source] crate::registry::UrlError),
138    /// The fleet root's object store could not be built.
139    #[error("failed to open fleet root store: {0}")]
140    FleetRootStore(#[source] object_store::Error),
141    /// A database or destination URL was rejected.
142    #[error(transparent)]
143    Url(#[from] crate::registry::UrlError),
144    /// An object-store operation failed.
145    #[error("object store error: {0}")]
146    Store(#[from] object_store::Error),
147    /// The database is not registered.
148    #[error("{url} is not registered; `sleet register` it first")]
149    NotRegistered {
150        /// The canonical database URL.
151        url: String,
152    },
153    /// No enabled mirror target of that name applies.
154    #[error("no enabled mirror target {target:?} applies to {url}")]
155    NoSuchMirrorTarget {
156        /// The requested target.
157        target: String,
158        /// The canonical database URL.
159        url: String,
160    },
161    /// A database service could not be opened or run.
162    #[error(transparent)]
163    Service(#[from] crate::services::ServiceError),
164    /// A mirror protocol operation failed.
165    #[error(transparent)]
166    Mirror(#[from] crate::mirror::MirrorError),
167    /// The node id is not valid for a heartbeat object name.
168    #[error("invalid node id: {0}")]
169    InvalidNodeId(String),
170    /// A node's offered service list contains a duplicate.
171    #[error("services lists {name:?} more than once", name = service.as_str())]
172    DuplicateService {
173        /// The repeated service.
174        service: Service,
175    },
176}
177
178impl From<crate::root::OpenError> for Error {
179    fn from(error: crate::root::OpenError) -> Self {
180        match error {
181            crate::root::OpenError::Url(source) => Self::FleetRootUrl(source),
182            crate::root::OpenError::Store(source) => Self::FleetRootStore(source),
183        }
184    }
185}
186
187impl From<ops::OpsError> for Error {
188    fn from(error: ops::OpsError) -> Self {
189        match error {
190            ops::OpsError::Url(source) => Self::Url(source),
191            ops::OpsError::Store(source) => Self::Store(source),
192            ops::OpsError::NotRegistered { url } => Self::NotRegistered { url },
193            ops::OpsError::NoSuchTarget { target, url } => Self::NoSuchMirrorTarget { target, url },
194            ops::OpsError::Service(source) => Self::Service(source),
195            ops::OpsError::Mirror(source) => Self::Mirror(source),
196        }
197    }
198}
199
200impl From<daemon::DaemonError> for Error {
201    fn from(error: daemon::DaemonError) -> Self {
202        match error {
203            daemon::DaemonError::NodeId(reason) => Self::InvalidNodeId(reason),
204            daemon::DaemonError::Root(source) => source.into(),
205        }
206    }
207}