pgdo/runtime/strategy.rs
1use std::collections::VecDeque;
2use std::env;
3use std::path::{Path, PathBuf};
4
5use super::{constraint::Constraint, Runtime};
6
7pub type Runtimes<'a> = Box<dyn Iterator<Item = Runtime> + 'a>;
8
9/// A strategy for finding PostgreSQL runtimes.
10///
11/// There are a few questions we want to answer:
12///
13/// 1. What runtimes are available?
14/// 2. Which of those runtimes is best suited to running a given cluster?
15/// 3. When there are no constraints, what runtime should we use?
16///
17/// This trait models those questions, and provides default implementations for
18/// #2 and #3.
19///
20/// However, a good place to start is the [`Default`] implementation of
21/// [`Strategy`]. It might do what you need.
22pub trait StrategyLike: std::fmt::Debug + std::panic::RefUnwindSafe + 'static {
23 /// Find all runtimes that this strategy knows about.
24 fn runtimes(&self) -> Runtimes<'_>;
25
26 /// Determine the most appropriate runtime known to this strategy for the
27 /// given constraint.
28 ///
29 /// The default implementation narrows the list of runtimes to those that
30 /// match the given constraint, then chooses the one with the highest
31 /// version number. It might return [`None`].
32 fn select(&self, constraint: &Constraint) -> Option<Runtime> {
33 self.runtimes()
34 .filter(|runtime| constraint.matches(runtime))
35 .max_by(|ra, rb| ra.version.cmp(&rb.version))
36 }
37
38 /// The runtime to use when there are no constraints, e.g. when creating a
39 /// new cluster.
40 ///
41 /// The default implementation selects the runtime with the highest version
42 /// number.
43 fn fallback(&self) -> Option<Runtime> {
44 self.runtimes().max_by(|ra, rb| ra.version.cmp(&rb.version))
45 }
46}
47
48/// Find runtimes on a given path.
49///
50/// Parses input according to platform conventions for the `PATH` environment
51/// variable. See [`env::split_paths`] for details.
52#[derive(Clone, Debug)]
53pub struct RuntimesOnPath(PathBuf);
54
55impl StrategyLike for RuntimesOnPath {
56 fn runtimes(&self) -> Runtimes<'_> {
57 Box::new(
58 env::split_paths(&self.0)
59 .filter(|bindir| bindir.join("pg_ctl").exists())
60 // Throw away runtimes that we can't determine the version for.
61 .filter_map(|bindir| Runtime::new(bindir).ok()),
62 )
63 }
64}
65
66/// Find runtimes on `PATH` (from the environment).
67#[derive(Clone, Debug)]
68pub struct RuntimesOnPathEnv;
69
70impl StrategyLike for RuntimesOnPathEnv {
71 fn runtimes(&self) -> Runtimes<'_> {
72 Box::new(
73 env::var_os("PATH")
74 .map(|path| {
75 env::split_paths(&path)
76 .filter(|bindir| bindir.join("pg_ctl").exists())
77 // Throw away runtimes that we can't determine the version for.
78 .filter_map(|bindir| Runtime::new(bindir).ok())
79 .collect::<Vec<_>>()
80 })
81 .unwrap_or_default()
82 .into_iter(),
83 )
84 }
85}
86
87/// Find runtimes using platform-specific knowledge.
88///
89/// For example:
90/// - on Debian and Ubuntu, check subdirectories of `/usr/lib/postgresql`.
91/// - on macOS, check Homebrew.
92///
93/// More platform-specific knowledge may be added to this strategy in the
94/// future.
95#[derive(Clone, Debug)]
96pub struct RuntimesOnPlatform;
97
98impl RuntimesOnPlatform {
99 /// Find runtimes using platform-specific knowledge (Linux).
100 ///
101 /// For example: on Debian and Ubuntu, check `/usr/lib/postgresql`.
102 #[cfg(any(doc, target_os = "linux"))]
103 pub fn find() -> Vec<PathBuf> {
104 glob::glob("/usr/lib/postgresql/*/bin/pg_ctl")
105 .ok()
106 .map(|entries| {
107 entries
108 .filter_map(Result::ok)
109 .filter(|path| path.is_file())
110 .filter_map(|path| path.parent().map(Path::to_owned))
111 .collect()
112 })
113 .unwrap_or_default()
114 }
115
116 /// Find runtimes using platform-specific knowledge (macOS).
117 ///
118 /// For example: check Homebrew.
119 #[cfg(any(doc, target_os = "macos"))]
120 pub fn find() -> Vec<PathBuf> {
121 use std::ffi::OsString;
122 use std::os::unix::ffi::OsStringExt;
123
124 std::process::Command::new("brew")
125 .arg("--prefix")
126 .output()
127 .ok()
128 .and_then(|output| {
129 if output.status.success() {
130 Some(OsString::from_vec(output.stdout))
131 } else {
132 None
133 }
134 })
135 .and_then(|brew_prefix| {
136 glob::glob(&format!(
137 "{}/Cellar/postgresql@*/*/bin/pg_ctl",
138 brew_prefix.to_string_lossy().trim_end()
139 ))
140 .ok()
141 })
142 .map(|entries| {
143 entries
144 .filter_map(Result::ok)
145 .filter(|path| path.is_file())
146 .filter_map(|path| path.parent().map(Path::to_owned))
147 .collect()
148 })
149 .unwrap_or_default()
150 }
151}
152
153impl StrategyLike for RuntimesOnPlatform {
154 fn runtimes(&self) -> Runtimes<'_> {
155 Box::new(
156 Self::find()
157 .into_iter()
158 // Throw away runtimes that we can't determine the version for.
159 .filter_map(|bindir| Runtime::new(bindir).ok()),
160 )
161 }
162}
163
164/// Compose strategies for finding PostgreSQL runtimes.
165#[derive(Debug)]
166pub enum Strategy {
167 /// Each strategy is consulted in turn.
168 Chain(VecDeque<Strategy>),
169 /// Delegate to another strategy; needed when implementing [`StrategyLike`].
170 Delegated(Box<dyn StrategyLike + Send + Sync>),
171 /// A single runtime; it always picks itself.
172 Single(Runtime),
173}
174
175impl Strategy {
176 /// Push the given strategy to the front of the chain.
177 ///
178 /// If this isn't already, it is converted into a [`Strategy::Chain`].
179 #[must_use]
180 pub fn push_front<S: Into<Strategy>>(mut self, strategy: S) -> Self {
181 match self {
182 Self::Chain(ref mut chain) => {
183 chain.push_front(strategy.into());
184 self
185 }
186 Self::Delegated(_) | Self::Single(_) => {
187 let mut chain: VecDeque<Strategy> = VecDeque::new();
188 chain.push_front(strategy.into());
189 chain.push_back(self);
190 Self::Chain(chain)
191 }
192 }
193 }
194
195 /// Push the given strategy to the back of the chain.
196 ///
197 /// If this isn't already, it is converted into a [`Strategy::Chain`].
198 #[must_use]
199 pub fn push_back<S: Into<Strategy>>(mut self, strategy: S) -> Self {
200 match self {
201 Self::Chain(ref mut chain) => {
202 chain.push_back(strategy.into());
203 self
204 }
205 Self::Delegated(_) | Self::Single(_) => {
206 let mut chain: VecDeque<Strategy> = VecDeque::new();
207 chain.push_front(self);
208 chain.push_back(strategy.into());
209 Self::Chain(chain)
210 }
211 }
212 }
213}
214
215impl Default for Strategy {
216 /// Select runtimes from on `PATH` followed by platform-specific runtimes.
217 fn default() -> Self {
218 Self::Chain(VecDeque::new())
219 .push_front(RuntimesOnPathEnv)
220 .push_back(RuntimesOnPlatform)
221 }
222}
223
224impl StrategyLike for Strategy {
225 /// - For a [`Strategy::Chain`], yields runtimes known to all strategies, in
226 /// the same order as each strategy returns them.
227 /// - For a [`Strategy::Delegated`], calls through to the wrapped strategy.
228 /// - For a [`Strategy::Single`], yields the runtime it's holding.
229 ///
230 /// **Note** that for the first two, runtimes are deduplicated by version
231 /// number, i.e. if a runtime with the same version number is yielded by
232 /// multiple strategies, or is yielded multiple times by a single strategy,
233 /// it will only be returned the first time it is seen.
234 fn runtimes(&self) -> Runtimes<'_> {
235 match self {
236 Self::Chain(chain) => {
237 let mut seen = std::collections::HashSet::new();
238 Box::new(
239 chain
240 .iter()
241 .flat_map(|strategy| strategy.runtimes())
242 .filter(move |runtime| seen.insert(runtime.version)),
243 )
244 }
245 Self::Delegated(strategy) => {
246 let mut seen = std::collections::HashSet::new();
247 Box::new(
248 strategy
249 .runtimes()
250 .filter(move |runtime| seen.insert(runtime.version)),
251 )
252 }
253 Self::Single(runtime) => Box::new(std::iter::once(runtime.clone())),
254 }
255 }
256
257 /// - For a [`Strategy::Chain`], asks each strategy in turn to select a
258 /// runtime. The first non-[`None`] answer is selected.
259 /// - For a [`Strategy::Delegated`], calls through to the wrapped strategy.
260 /// - For a [`Strategy::Single`], returns the runtime if it's compatible.
261 fn select(&self, constraint: &Constraint) -> Option<Runtime> {
262 match self {
263 Self::Chain(c) => c.iter().find_map(|strategy| strategy.select(constraint)),
264 Self::Delegated(strategy) => strategy.select(constraint),
265 Self::Single(runtime) if constraint.matches(runtime) => Some(runtime.clone()),
266 Self::Single(_) => None,
267 }
268 }
269
270 /// - For a [`Strategy::Chain`], asks each strategy in turn for a fallback
271 /// runtime. The first non-[`None`] answer is selected.
272 /// - For a [`Strategy::Delegated`], calls through to the wrapped strategy.
273 /// - For a [`Strategy::Single`], returns the runtime it's holding.
274 fn fallback(&self) -> Option<Runtime> {
275 match self {
276 Self::Chain(chain) => chain.iter().find_map(Strategy::fallback),
277 Self::Delegated(strategy) => strategy.fallback(),
278 Self::Single(runtime) => Some(runtime.clone()),
279 }
280 }
281}
282
283impl From<RuntimesOnPath> for Strategy {
284 /// Converts the given strategy into a [`Strategy::Delegated`].
285 fn from(strategy: RuntimesOnPath) -> Self {
286 Self::Delegated(Box::new(strategy))
287 }
288}
289
290impl From<RuntimesOnPathEnv> for Strategy {
291 /// Converts the given strategy into a [`Strategy::Delegated`].
292 fn from(strategy: RuntimesOnPathEnv) -> Self {
293 Self::Delegated(Box::new(strategy))
294 }
295}
296
297impl From<RuntimesOnPlatform> for Strategy {
298 /// Converts the given strategy into a [`Strategy::Delegated`].
299 fn from(strategy: RuntimesOnPlatform) -> Self {
300 Self::Delegated(Box::new(strategy))
301 }
302}
303
304impl From<Runtime> for Strategy {
305 /// Converts the given runtime into a [`Strategy::Single`].
306 fn from(runtime: Runtime) -> Self {
307 Self::Single(runtime)
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use std::env;
314
315 use super::{RuntimesOnPath, RuntimesOnPathEnv, RuntimesOnPlatform, Strategy, StrategyLike};
316
317 /// This will fail if there are no PostgreSQL runtimes installed.
318 #[test]
319 fn runtime_find_custom_path() {
320 let path = env::var_os("PATH").expect("PATH not set");
321 let strategy = RuntimesOnPath(path.into());
322 let runtimes = strategy.runtimes();
323 assert_ne!(0, runtimes.count());
324 }
325
326 /// This will fail if there are no PostgreSQL runtimes installed.
327 #[test]
328 fn runtime_find_env_path() {
329 let runtimes = RuntimesOnPathEnv.runtimes();
330 assert_ne!(0, runtimes.count());
331 }
332
333 /// This will fail if there are no PostgreSQL runtimes installed.
334 #[test]
335 #[cfg(any(target_os = "linux", target_os = "macos"))]
336 fn runtime_find_on_platform() {
337 let runtimes = RuntimesOnPlatform.runtimes();
338 assert_ne!(0, runtimes.count());
339 }
340
341 /// This will fail if there are no PostgreSQL runtimes installed. It's also
342 /// somewhat fragile because it relies upon knowing the implementation of
343 /// the strategies of which the default [`StrategySet`] is composed.
344 #[test]
345 fn runtime_strategy_set_default() {
346 let strategy = Strategy::default();
347 // There is at least one runtime available.
348 let runtimes = strategy.runtimes();
349 assert_ne!(0, runtimes.count());
350 // There is always a fallback.
351 assert!(strategy.fallback().is_some());
352 }
353}