@@ -534,7 +534,8 @@ public actor Orchestrator {
534534 progressHandler: ProgressUpdateHandler ? = nil ,
535535 pullPolicy: PullPolicy = . missing,
536536 wait: Bool = false ,
537- waitTimeoutSeconds: Int ? = nil
537+ waitTimeoutSeconds: Int ? = nil ,
538+ disableHealthcheck: Bool = false
538539 ) async throws {
539540 log. info ( " Starting project ' \( project. name) ' " )
540541
@@ -595,14 +596,15 @@ public actor Orchestrator {
595596 noRecreate: noRecreate,
596597 removeOnExit: removeOnExit,
597598 progressHandler: progressHandler,
598- pullPolicy: pullPolicy
599+ pullPolicy: pullPolicy,
600+ disableHealthcheck: disableHealthcheck
599601 )
600602
601603 // If --wait is set, wait for selected services to be healthy/running
602604 if wait {
603605 let timeout = waitTimeoutSeconds ?? 300
604606 for (name, svc) in targetServices {
605- if svc. healthCheck != nil {
607+ if !disableHealthcheck , svc. healthCheck != nil {
606608 try await waitUntilHealthy ( project: project, serviceName: name, service: svc)
607609 } else {
608610 let cid = svc. containerName ?? " \( project. name) _ \( name) "
@@ -667,7 +669,8 @@ public actor Orchestrator {
667669 noRecreate: Bool ,
668670 removeOnExit: Bool ,
669671 progressHandler: ProgressUpdateHandler ? ,
670- pullPolicy: PullPolicy
672+ pullPolicy: PullPolicy ,
673+ disableHealthcheck: Bool
671674 ) async throws {
672675 // Sort services by dependencies
673676 let resolution = try DependencyResolver . resolve ( services: services)
@@ -678,7 +681,7 @@ public actor Orchestrator {
678681
679682 do {
680683 // Wait for dependency conditions before starting this service
681- try await waitForDependencyConditions ( project: project, serviceName: serviceName, services: services)
684+ try await waitForDependencyConditions ( project: project, serviceName: serviceName, services: services, disableHealthcheck : disableHealthcheck )
682685
683686 try await createAndStartContainer (
684687 project: project,
@@ -692,12 +695,8 @@ public actor Orchestrator {
692695 pullPolicy: pullPolicy
693696 )
694697
695- // Optionally kick health monitor for the service
696- if service. healthCheck != nil {
697- Task { [ weak self] in
698- _ = try ? await self ? . runHealthCheckOnce ( project: project, serviceName: serviceName, service: service)
699- }
700- }
698+ // Do not run background health probes by default.
699+ // Health checks are evaluated only when --wait is explicitly requested.
701700 } catch {
702701 log. error ( " Failed to start service ' \( serviceName) ': \( error) " )
703702 throw error
@@ -706,7 +705,7 @@ public actor Orchestrator {
706705 }
707706
708707 /// Wait for dependencies according to compose depends_on conditions
709- private func waitForDependencyConditions( project: Project , serviceName: String , services: [ String : Service ] ) async throws {
708+ private func waitForDependencyConditions( project: Project , serviceName: String , services: [ String : Service ] , disableHealthcheck : Bool ) async throws {
710709 guard let svc = services [ serviceName] else { return }
711710
712711 // Wait for service_started
@@ -715,7 +714,7 @@ public actor Orchestrator {
715714 try await waitUntilContainerRunning ( containerId: depId, timeoutSeconds: 120 )
716715 }
717716 // Wait for service_healthy
718- for dep in svc. dependsOnHealthy {
717+ for dep in svc. dependsOnHealthy where !disableHealthcheck {
719718 if let depSvc = services [ dep] {
720719 try await waitUntilHealthy ( project: project, serviceName: dep, service: depSvc)
721720 }
@@ -903,16 +902,24 @@ public actor Orchestrator {
903902
904903 // Recreate the container
905904 log. info ( " Recreating existing container ' \( existing. id) ' for service ' \( serviceName) ' " )
906- // Stop with a longer timeout and wait until it's fully stopped before deleting
905+ // 1) Ask it to stop gracefully (SIGTERM) with longer timeout
907906 do { try await existing. stop ( opts: ContainerStopOptions ( timeoutInSeconds: 15 , signal: SIGTERM) ) }
908907 catch { log. warning ( " failed to stop \( existing. id) : \( error) " ) }
909- do { try await waitUntilContainerStopped ( containerId: existing. id, timeoutSeconds: 20 ) }
910- catch { log. warning ( " timeout waiting for \( existing. id) to stop: \( error) " ) }
908+ // 2) Wait until it is actually stopped; if not, escalate to SIGKILL and wait briefly
909+ do {
910+ try await waitUntilContainerStopped ( containerId: existing. id, timeoutSeconds: 20 )
911+ } catch {
912+ log. warning ( " timeout waiting for \( existing. id) to stop: \( error) ; sending SIGKILL " )
913+ do { try await existing. kill ( SIGKILL) } catch { log. warning ( " failed to SIGKILL \( existing. id) : \( error) " ) }
914+ // small wait after SIGKILL
915+ try ? await Task . sleep ( nanoseconds: 700_000_000 )
916+ }
917+ // 3) Try to delete (force on retry)
911918 do { try await existing. delete ( ) }
912919 catch {
913- log. warning ( " failed to delete \( existing. id) : \( error) ; retrying once after short delay " )
920+ log. warning ( " failed to delete \( existing. id) : \( error) ; retrying forced delete after short delay " )
914921 try ? await Task . sleep ( nanoseconds: 700_000_000 )
915- do { try await existing. delete ( ) } catch { log. warning ( " second delete attempt failed for \( existing. id) : \( error) " ) }
922+ do { try await existing. delete ( force : true ) } catch { log. warning ( " forced delete attempt failed for \( existing. id) : \( error) " ) }
916923 }
917924 projectState [ project. name] ? . containers. removeValue ( forKey: serviceName)
918925 }
@@ -1128,12 +1135,25 @@ public actor Orchestrator {
11281135 // Add volume mounts (ensure named/anonymous volumes exist and use their host paths)
11291136 config. mounts = try await resolveComposeMounts ( project: project, serviceName: serviceName, mounts: service. volumes)
11301137
1131- // Add resource limits
1138+ // Add resource limits (compose-style parsing for memory like "2g", "2048MB").
11321139 if let cpus = service. cpus {
11331140 config. resources. cpus = Int ( cpus) ?? 4
11341141 }
1135- if let memory = service. memory {
1136- config. resources. memoryInBytes = UInt64 ( memory) ?? 1024 . mib ( )
1142+ if let memStr = service. memory, !memStr. isEmpty {
1143+ do {
1144+ if memStr. lowercased ( ) == " max " {
1145+ // Treat "max" as no override: keep the runtime/default value (set below if needed).
1146+ // Intentionally do nothing here.
1147+ } else {
1148+ let res = try Parser . resources ( cpus: nil , memory: memStr)
1149+ if let bytes = res. memoryInBytes as UInt64 ? { config. resources. memoryInBytes = bytes }
1150+ }
1151+ } catch {
1152+ log. warning ( " Invalid memory value ' \\ (memStr)'; using default. Error: \\ (error) " )
1153+ }
1154+ } else {
1155+ // Safer default for dev servers (was 1 GiB)
1156+ config. resources. memoryInBytes = 2048 . mib ( )
11371157 }
11381158
11391159 // TTY support from compose service
@@ -1670,7 +1690,8 @@ public actor Orchestrator {
16701690 services: [ String ] = [ ] ,
16711691 follow: Bool = false ,
16721692 tail: Int ? = nil ,
1673- timestamps: Bool = false
1693+ timestamps: Bool = false ,
1694+ includeBoot: Bool = false
16741695 ) async throws -> AsyncThrowingStream < LogEntry , Error > {
16751696 // Resolve target services
16761697 let selected = services. isEmpty ? Set ( project. services. keys) : Set ( services)
@@ -1702,6 +1723,8 @@ public actor Orchestrator {
17021723
17031724 final class Emitter : @unchecked Sendable {
17041725 let cont : AsyncThrowingStream < LogEntry , Error > . Continuation
1726+ // Strongly retain file handles so readabilityHandler keeps firing.
1727+ private var retained : [ FileHandle ] = [ ]
17051728 init ( _ c: AsyncThrowingStream < LogEntry , Error > . Continuation ) { self . cont = c }
17061729 func emit( _ svc: String , _ containerName: String , _ stream: LogEntry . LogStream , data: Data ) {
17071730 guard !data. isEmpty else { return }
@@ -1711,13 +1734,11 @@ public actor Orchestrator {
17111734 }
17121735 }
17131736 }
1737+ func retain( _ fh: FileHandle ) { retained. append ( fh) }
17141738 func finish( ) { cont. finish ( ) }
17151739 func fail( _ error: Error ) { cont. yield ( with: . failure( error) ) }
17161740 }
17171741 let emitter = Emitter ( continuation)
1718- // Retain FileHandles so readabilityHandler continues firing while the stream is active
1719- actor HandleRetainer { var fhs : [ FileHandle ] = [ ] ; func add( _ fh: FileHandle ) { fhs. append ( fh) } }
1720- let retainer = HandleRetainer ( )
17211742 actor Counter { var value : Int ; init ( _ v: Int ) { value = v } ; func dec( ) -> Int { value -= 1 ; return value } }
17221743 let counter = Counter ( targets. count)
17231744
@@ -1732,20 +1753,20 @@ public actor Orchestrator {
17321753 fh. readabilityHandler = { handle in
17331754 emitter. emit ( svc, container. id, . stdout, data: handle. availableData)
17341755 }
1735- await retainer . add ( fh)
1756+ emitter . retain ( fh)
17361757 }
1737- if fds. indices. contains ( 1 ) {
1758+ if includeBoot , fds. indices. contains ( 1 ) {
17381759 let fh = fds [ 1 ]
17391760 fh. readabilityHandler = { handle in
17401761 emitter. emit ( svc, container. id, . stderr, data: handle. availableData)
17411762 }
1742- await retainer . add ( fh)
1763+ emitter . retain ( fh)
17431764 }
17441765 } else {
17451766 if fds. indices. contains ( 0 ) {
17461767 emitter. emit ( svc, container. id, . stdout, data: fds [ 0 ] . readDataToEndOfFile ( ) )
17471768 }
1748- if fds. indices. contains ( 1 ) {
1769+ if includeBoot , fds. indices. contains ( 1 ) {
17491770 emitter. emit ( svc, container. id, . stderr, data: fds [ 1 ] . readDataToEndOfFile ( ) )
17501771 }
17511772 let left = await counter. dec ( )
@@ -1765,10 +1786,11 @@ public actor Orchestrator {
17651786 public func start(
17661787 project: Project ,
17671788 services: [ String ] = [ ] ,
1789+ disableHealthcheck: Bool = false ,
17681790 progressHandler: ProgressUpdateHandler ? = nil
17691791 ) async throws {
1770- // For build functionality, we just call up
1771- try await up ( project: project, services: services, progressHandler: progressHandler)
1792+ // Reuse up() path with defaults
1793+ try await up ( project: project, services: services, progressHandler: progressHandler, disableHealthcheck : disableHealthcheck )
17721794 }
17731795
17741796
@@ -1789,10 +1811,11 @@ public actor Orchestrator {
17891811 project: Project ,
17901812 services: [ String ] = [ ] ,
17911813 timeout: Int = 10 ,
1814+ disableHealthcheck: Bool = false ,
17921815 progressHandler: ProgressUpdateHandler ? = nil
17931816 ) async throws {
17941817 _ = try await down ( project: project, progressHandler: progressHandler)
1795- try await up ( project: project, services: services, progressHandler: progressHandler)
1818+ try await up ( project: project, services: services, progressHandler: progressHandler, disableHealthcheck : disableHealthcheck )
17961819 }
17971820
17981821 /// Execute command in a service
0 commit comments