@@ -16,7 +16,7 @@ use tracing::{debug, info, warn};
1616use crate :: error:: StackError ;
1717use crate :: events:: StackEvent ;
1818use crate :: executor:: ContainerRuntime ;
19- use crate :: spec:: { HealthCheckSpec , ServiceSpec , StackSpec } ;
19+ use crate :: spec:: { DependencyCondition , HealthCheckSpec , ServiceSpec , StackSpec } ;
2020use crate :: state_store:: { ServiceObservedState , ServicePhase , StateStore } ;
2121
2222/// Result of checking whether a service's dependencies are satisfied.
@@ -103,11 +103,14 @@ pub fn is_service_ready(
103103/// only blocks when:
104104///
105105/// - It is in a terminal state (`Failed` / `Stopped`).
106- /// - It is `Running` with a health check that has not yet passed.
106+ /// - The condition is `service_healthy` and the health check has
107+ /// not yet passed.
108+ /// - The condition is `service_completed_successfully` and the
109+ /// service has not exited with code 0.
107110///
108- /// This means a fresh deployment creates all services in one
109- /// topo-sorted pass, while health-checked dependencies gate their
110- /// dependents across apply cycles .
111+ /// With the default `service_started` condition, a running service
112+ /// is considered ready regardless of health check status — matching
113+ /// Docker Compose semantics .
111114pub fn check_dependencies (
112115 service : & ServiceSpec ,
113116 observed : & [ ServiceObservedState ] ,
@@ -128,10 +131,10 @@ pub fn check_dependencies(
128131
129132 let mut waiting_on = Vec :: new ( ) ;
130133
131- for dep_name in & service. depends_on {
132- let dep_obs = observed_map. get ( dep_name . as_str ( ) ) ;
133- let dep_spec = spec_map. get ( dep_name . as_str ( ) ) ;
134- let dep_health = health_statuses. get ( dep_name ) ;
134+ for dep in & service. depends_on {
135+ let dep_obs = observed_map. get ( dep . service . as_str ( ) ) ;
136+ let dep_spec = spec_map. get ( dep . service . as_str ( ) ) ;
137+ let dep_health = health_statuses. get ( & dep . service ) ;
135138
136139 let blocked = match dep_obs {
137140 None => {
@@ -141,21 +144,32 @@ pub fn check_dependencies(
141144 Some ( obs) => match obs. phase {
142145 // Terminal states block dependent creation.
143146 ServicePhase :: Failed | ServicePhase :: Stopped => true ,
144- // Running: block only if there's a health check that hasn't passed.
145- ServicePhase :: Running => {
146- let healthcheck = dep_spec. and_then ( |s| s. healthcheck . as_ref ( ) ) ;
147- match healthcheck {
148- None => false , // Running + no health check = ready.
149- Some ( hc) => !is_service_ready ( obs, Some ( hc) , dep_health) ,
147+ // Running: behaviour depends on the condition.
148+ ServicePhase :: Running => match dep. condition {
149+ DependencyCondition :: ServiceStarted => {
150+ // Running is sufficient — don't check health.
151+ false
150152 }
151- }
153+ DependencyCondition :: ServiceHealthy => {
154+ // Must have a passing health check.
155+ let healthcheck = dep_spec. and_then ( |s| s. healthcheck . as_ref ( ) ) ;
156+ match healthcheck {
157+ None => false , // No health check defined = ready.
158+ Some ( hc) => !is_service_ready ( obs, Some ( hc) , dep_health) ,
159+ }
160+ }
161+ DependencyCondition :: ServiceCompletedSuccessfully => {
162+ // Running means not completed yet → blocked.
163+ true
164+ }
165+ } ,
152166 // Pending/Creating/Stopping — in progress, don't block.
153167 _ => false ,
154168 } ,
155169 } ;
156170
157171 if blocked {
158- waiting_on. push ( dep_name . clone ( ) ) ;
172+ waiting_on. push ( dep . service . clone ( ) ) ;
159173 }
160174 }
161175
@@ -433,6 +447,7 @@ mod tests {
433447 #![ allow( clippy:: unwrap_used) ]
434448
435449 use super :: * ;
450+ use crate :: spec:: ServiceDependency ;
436451 use std:: collections:: HashMap ;
437452
438453 fn svc ( name : & str ) -> ServiceSpec {
@@ -457,7 +472,14 @@ mod tests {
457472
458473 fn svc_with_deps ( name : & str , deps : Vec < & str > ) -> ServiceSpec {
459474 ServiceSpec {
460- depends_on : deps. into_iter ( ) . map ( String :: from) . collect ( ) ,
475+ depends_on : deps. into_iter ( ) . map ( ServiceDependency :: started) . collect ( ) ,
476+ ..svc ( name)
477+ }
478+ }
479+
480+ fn svc_with_healthy_deps ( name : & str , deps : Vec < & str > ) -> ServiceSpec {
481+ ServiceSpec {
482+ depends_on : deps. into_iter ( ) . map ( ServiceDependency :: healthy) . collect ( ) ,
461483 ..svc ( name)
462484 }
463485 }
@@ -644,12 +666,25 @@ mod tests {
644666 }
645667
646668 #[ test]
647- fn dep_running_with_healthcheck_failing_is_blocked ( ) {
669+ fn dep_running_with_healthcheck_service_started_is_ready ( ) {
670+ // service_started condition: running is sufficient, healthcheck irrelevant.
648671 let service = svc_with_deps ( "web" , vec ! [ "db" ] ) ;
649672 let all_services = vec ! [ svc_with_healthcheck( "db" ) , service. clone( ) ] ;
650673 let observed = vec ! [ obs( "db" , ServicePhase :: Running ) ] ;
651674
652- // No health status means health check hasn't passed yet.
675+ // No health status — but condition is service_started, so not blocked.
676+ let result = check_dependencies ( & service, & observed, & all_services, & HashMap :: new ( ) ) ;
677+ assert_eq ! ( result, DependencyCheck :: Ready ) ;
678+ }
679+
680+ #[ test]
681+ fn dep_running_with_healthcheck_service_healthy_blocks ( ) {
682+ // service_healthy condition: must wait for health check to pass.
683+ let service = svc_with_healthy_deps ( "web" , vec ! [ "db" ] ) ;
684+ let all_services = vec ! [ svc_with_healthcheck( "db" ) , service. clone( ) ] ;
685+ let observed = vec ! [ obs( "db" , ServicePhase :: Running ) ] ;
686+
687+ // No health status means health check hasn't passed yet → blocked.
653688 let result = check_dependencies ( & service, & observed, & all_services, & HashMap :: new ( ) ) ;
654689 assert_eq ! (
655690 result,
@@ -660,8 +695,9 @@ mod tests {
660695 }
661696
662697 #[ test]
663- fn dep_running_with_healthcheck_passing_is_ready ( ) {
664- let service = svc_with_deps ( "web" , vec ! [ "db" ] ) ;
698+ fn dep_running_with_healthcheck_passing_service_healthy_is_ready ( ) {
699+ // service_healthy condition + health check passed → ready.
700+ let service = svc_with_healthy_deps ( "web" , vec ! [ "db" ] ) ;
665701 let all_services = vec ! [ svc_with_healthcheck( "db" ) , service. clone( ) ] ;
666702 let observed = vec ! [ obs( "db" , ServicePhase :: Running ) ] ;
667703
0 commit comments