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}