1use std::{path::PathBuf, time::Duration};
2
3use crate::{
4 DaemonControl, DaemonLaunchConfig, DaemonLaunchLog, PlacementSummary, Result, RuntimeDatabase,
5 Session, SessionConfig, SessionRequirements,
6 acquisition::SessionAcquisition,
7 placement::{PlacementEnvironment, PlacementResolver, PlacementSpec},
8};
9
10#[derive(Debug, Clone)]
11pub struct Runtime {
12 config: Config,
13 placement: PlacementSpec,
14}
15
16impl Runtime {
17 pub fn try_new(config: Config) -> Result<Self> {
18 let placement =
19 PlacementResolver::with_environment(config.placement_environment()).resolve(&config)?;
20
21 Ok(Self { config, placement })
22 }
23
24 pub fn config(&self) -> &Config {
25 &self.config
26 }
27
28 pub(crate) fn placement(&self) -> &PlacementSpec {
29 &self.placement
30 }
31
32 pub fn placement_summary(&self) -> PlacementSummary {
33 PlacementSummary::from_placement(&self.placement)
34 }
35
36 pub async fn acquire_session(&self) -> Result<Session> {
37 SessionAcquisition::new(self).acquire().await
38 }
39
40 pub fn daemon(&self) -> DaemonControl<'_> {
41 DaemonControl::new(self)
42 }
43}
44
45#[derive(Debug, Clone)]
46pub struct Config {
47 database: RuntimeDatabase,
48 client: ApiClientConfig,
49 session: SessionConfig,
50 daemon: DaemonLaunchConfig,
51 requirements: SessionRequirements,
52 placement_environment: PlacementEnvironment,
53}
54
55impl Config {
56 pub fn new(database: RuntimeDatabase) -> Self {
57 Self {
58 database,
59 client: ApiClientConfig::default(),
60 session: SessionConfig::default(),
61 daemon: DaemonLaunchConfig::default(),
62 requirements: SessionRequirements::default(),
63 placement_environment: PlacementEnvironment::capture(),
64 }
65 }
66
67 #[must_use]
68 pub fn with_api_client(mut self, client: ApiClientConfig) -> Self {
69 self.client = client;
70 self
71 }
72
73 #[must_use]
74 pub fn with_api_timeout(
75 self,
76 request_timeout: Duration,
77 user_agent: impl Into<String>,
78 ) -> Self {
79 self.with_api_client(ApiClientConfig::new(request_timeout, user_agent))
80 }
81
82 #[must_use]
83 pub fn with_session(mut self, session: SessionConfig) -> Self {
84 self.session = session;
85 self
86 }
87
88 #[must_use]
89 pub fn with_session_timeout(self, acquire_timeout: Duration) -> Self {
90 self.with_session(SessionConfig::new(acquire_timeout))
91 }
92
93 #[must_use]
94 pub fn with_daemon_launch(mut self, daemon: DaemonLaunchConfig) -> Self {
95 self.daemon = daemon;
96 self
97 }
98
99 #[must_use]
100 pub fn with_daemon_log(mut self, log: impl Into<PathBuf>) -> Self {
101 self.daemon = self.daemon.with_log(DaemonLaunchLog::file(log));
102 self
103 }
104
105 #[must_use]
106 pub fn with_daemon_session_lease_duration(mut self, lease_duration: Duration) -> Self {
107 self.daemon = self.daemon.with_session_lease_duration(lease_duration);
108 self
109 }
110
111 #[must_use]
112 pub fn with_daemon_session_idle_shutdown_grace(
113 mut self,
114 idle_shutdown_grace: Duration,
115 ) -> Self {
116 self.daemon = self
117 .daemon
118 .with_session_idle_shutdown_grace(idle_shutdown_grace);
119 self
120 }
121
122 #[must_use]
123 pub fn with_runtime_root(mut self, root: impl Into<PathBuf>) -> Self {
124 self.placement_environment = PlacementEnvironment::from_root(root);
125 self
126 }
127
128 #[must_use]
129 pub fn with_requirements(mut self, requirements: SessionRequirements) -> Self {
130 self.requirements = requirements;
131 self
132 }
133
134 pub fn database(&self) -> &RuntimeDatabase {
135 &self.database
136 }
137
138 pub fn client(&self) -> &ApiClientConfig {
139 &self.client
140 }
141
142 pub fn session(&self) -> &SessionConfig {
143 &self.session
144 }
145
146 pub fn daemon(&self) -> &DaemonLaunchConfig {
147 &self.daemon
148 }
149
150 pub fn requirements(&self) -> &SessionRequirements {
151 &self.requirements
152 }
153
154 pub(crate) fn placement_environment(&self) -> PlacementEnvironment {
155 self.placement_environment.clone()
156 }
157
158 #[cfg(test)]
159 pub(crate) fn with_placement_environment(
160 mut self,
161 placement_environment: PlacementEnvironment,
162 ) -> Self {
163 self.placement_environment = placement_environment;
164 self
165 }
166}
167
168#[derive(Debug, Clone, PartialEq, Eq)]
170pub struct ApiClientConfig {
171 request_timeout: Duration,
172 user_agent: String,
173}
174
175impl ApiClientConfig {
176 pub fn new(request_timeout: Duration, user_agent: impl Into<String>) -> Self {
177 Self {
178 request_timeout,
179 user_agent: user_agent.into(),
180 }
181 }
182
183 pub fn request_timeout(&self) -> Duration {
184 self.request_timeout
185 }
186
187 pub fn user_agent(&self) -> &str {
188 &self.user_agent
189 }
190}
191
192impl Default for ApiClientConfig {
193 fn default() -> Self {
194 Self::new(
195 Duration::from_secs(30),
196 concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")),
197 )
198 }
199}
200
201#[cfg(test)]
202mod tests {
203 use std::time::Duration;
204
205 use crate::{
206 DaemonExecutable, DaemonLaunchConfig, DaemonLaunchLog, Runtime, RuntimeConfig,
207 RuntimeDatabase,
208 };
209
210 mod daemon_launch {
211 use super::*;
212
213 #[test]
214 fn keeps_config() -> crate::Result<()> {
215 let tempdir = tempfile::tempdir()?;
216 let config =
217 RuntimeConfig::new(RuntimeDatabase::sqlite(tempdir.path().join("synd.db")))
218 .with_api_timeout(Duration::from_secs(5), "synd-runtime-test")
219 .with_session_timeout(Duration::from_secs(5))
220 .with_daemon_launch(DaemonLaunchConfig::default());
221
222 let runtime = Runtime::try_new(config.clone())?;
223
224 assert_eq!(runtime.config().daemon(), config.daemon());
225 Ok(())
226 }
227 }
228
229 mod daemon_log {
230 use super::*;
231
232 #[test]
233 fn keeps_executable() -> crate::Result<()> {
234 let tempdir = tempfile::tempdir()?;
235 let log = tempdir.path().join("daemon.log");
236 let config =
237 RuntimeConfig::new(RuntimeDatabase::sqlite(tempdir.path().join("synd.db")))
238 .with_daemon_launch(DaemonLaunchConfig::new(
239 DaemonExecutable::path("/usr/bin/custom-synd"),
240 DaemonLaunchLog::file(tempdir.path().join("old.log")),
241 ))
242 .with_daemon_log(log.clone());
243
244 assert_eq!(
245 config.daemon(),
246 &DaemonLaunchConfig::new(
247 DaemonExecutable::path("/usr/bin/custom-synd"),
248 DaemonLaunchLog::file(log),
249 )
250 );
251 Ok(())
252 }
253 }
254}