use std::thread; use std::time::{Duration, Instant}; use anyhow::{Result, bail}; use super::shape::Kind; use crate::cmd::{Cmd, PgIsReady, PgRestore, Psql}; use crate::ctx::Ctx; use crate::project::Project; const READY_TIMEOUT: Duration = Duration::from_secs(60); const POLL_INTERVAL: Duration = Duration::from_secs(1); // stands in for an id a dry run has no running container to look up const PLANNED_CONTAINER: &str = ""; pub(super) struct Database { pub(super) service: String, pub(super) container: String, pub(super) user: String, pub(super) name: String, } impl Database { pub(super) fn resolve(ctx: &Ctx) -> Result { let compose = Project::resolve(ctx)?; let service = compose.postgres()?; let (user, name) = compose.postgres_credentials(&service)?; let listed = ctx.compose().ps(&service).capture(ctx)?; let mut ids = listed.lines().map(str::trim).filter(|id| !id.is_empty()); let container = match (ids.next(), ids.next()) { (Some(id), None) => id.to_string(), // one id per line: a scaled service has several, and the whole // listing would go to docker exec as though it were a single id (Some(_), Some(_)) => bail!( "service {service} has more than one container running; \ scale it to one first" ), // a dry run only prints a plan, and it is worth printing with the // project down, which is when it is most likely to be asked for (None, _) if ctx.dry_run => PLANNED_CONTAINER.to_string(), (None, _) => bail!("service {service} has no running container"), }; Ok(Self { service, container, user, name, }) } // what reads this shape of dump back in pub(super) fn restore_with(&self, kind: Kind) -> Box { match kind { Kind::Archive => Box::new(PgRestore::new(&self.user, &self.name)), Kind::Sql => Box::new(Psql::new(&self.user, &self.name).quiet().atomic()), Kind::Cluster => Box::new(Psql::new(&self.user, "postgres").quiet()), } } } pub(super) fn wait_until_ready(ctx: &Ctx, db: &Database) -> Result<()> { if ctx.dry_run { return Ok(()); } let deadline = Instant::now() + READY_TIMEOUT; loop { let ready = PgIsReady { username: &db.user, dbname: &db.name, } .in_container(&db.container) .quietly_succeeds(ctx)?; if ready { return Ok(()); } if Instant::now() >= deadline { bail!( "{} did not accept connections within {} seconds", db.service, READY_TIMEOUT.as_secs() ); } thread::sleep(POLL_INTERVAL); } } pub(super) fn when_ready(ctx: &Ctx, db: &Database, command: &dyn Cmd) -> Result<()> { wait_until_ready(ctx, db)?; command.run(ctx) }