use std::collections::{HashMap, HashSet};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use futures_util::future::{FutureExt, Shared};
use crate::compose::types::{ComposeFile, ServiceCondition};
use crate::engine::Engine;
use crate::error::ComposeError;
use super::in_started_set;
pub(super) type SharedReady<'a> =
Shared<Pin<Box<dyn Future<Output = std::result::Result<(), Arc<ComposeError>>> + Send + 'a>>>;
impl Engine {
pub(super) fn build_readiness_map<'a>(
&'a self,
file: &'a ComposeFile,
enabled: &HashSet<String>,
target_set: &Option<HashSet<String>>,
start: bool,
) -> HashMap<String, SharedReady<'a>> {
let mut map: HashMap<String, SharedReady<'a>> = HashMap::new();
if !start {
return map;
}
for (sname, service) in &file.services {
if let Some(set) = target_set {
if !set.contains(sname) {
continue;
}
}
if !enabled.contains(sname) {
continue;
}
for dep in service.depends_on.service_names() {
if !matches!(
service.depends_on.condition_for(&dep),
ServiceCondition::ServiceHealthy
) {
continue;
}
if !in_started_set(target_set, &dep) {
continue;
}
let Some(dep_service) = file.services.get(&dep) else {
continue;
};
if !enabled.contains(&dep) {
continue;
}
if dep_service
.healthcheck
.as_ref()
.is_some_and(|h| h.is_disabled())
{
continue;
}
let container = self.first_replica_name(&dep, dep_service);
map.entry(container.clone()).or_insert_with(|| {
let c = container.clone();
async move {
self.wait_healthy(&c, dep_service, None)
.await
.map_err(Arc::new)
}
.boxed()
.shared()
});
}
}
map
}
}
#[cfg(all(test, unix))]
mod tests {
use std::collections::HashSet;
use crate::engine::Engine;
use crate::libpod::Client;
fn engine(project: &str) -> Engine {
let client = Client::new("/tmp/podup-readiness-test.sock");
Engine::with_base_dir(client, project.into(), std::env::temp_dir())
}
fn enabled_all(file: &crate::compose::types::ComposeFile) -> HashSet<String> {
file.services.keys().cloned().collect()
}
#[test]
fn shares_one_poller_per_service_healthy_container() {
let yaml = "\
services:
db:
image: x
healthcheck:
test: [\"CMD\", \"true\"]
cache:
image: x
web:
image: x
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
api:
image: x
depends_on:
db:
condition: service_healthy
";
let file = crate::compose::parse_str(yaml).unwrap();
let e = engine("proj");
let map = e.build_readiness_map(&file, &enabled_all(&file), &None, true);
let keys: Vec<&String> = map.keys().collect();
assert_eq!(map.len(), 1, "one shared poller expected, got {keys:?}");
assert!(
keys[0].contains("db"),
"shared container should be db, got {keys:?}"
);
}
#[test]
fn create_only_shares_nothing() {
let yaml = "\
services:
db:
image: x
healthcheck:
test: [\"CMD\", \"true\"]
web:
image: x
depends_on:
db:
condition: service_healthy
";
let file = crate::compose::parse_str(yaml).unwrap();
let e = engine("proj");
assert!(e
.build_readiness_map(&file, &enabled_all(&file), &None, false)
.is_empty());
}
#[test]
fn disabled_healthcheck_is_not_shared() {
let yaml = "\
services:
db:
image: x
healthcheck:
disable: true
web:
image: x
depends_on:
db:
condition: service_healthy
";
let file = crate::compose::parse_str(yaml).unwrap();
let e = engine("proj");
assert!(
e.build_readiness_map(&file, &enabled_all(&file), &None, true)
.is_empty(),
"a disabled healthcheck must not be shared or polled"
);
}
}