Skip to content

feat: launch independent services concurrently in up (#128) - #145

Open
OrtegaMatias wants to merge 2 commits into
Mcrich23:mainfrom
OrtegaMatias:feat/parallel-up-launch
Open

feat: launch independent services concurrently in up (#128)#145
OrtegaMatias wants to merge 2 commits into
Mcrich23:mainfrom
OrtegaMatias:feat/parallel-up-launch

Conversation

@OrtegaMatias

Copy link
Copy Markdown
Contributor

Summary

Fixes #128: up launched every service strictly one at a time, so N independent services paid N× the per-service start + readiness cost (the issue reports ~23s for 11 independent services). This launches each dependency wave concurrently — services with no pending dependency start together, while a dependent still waits for its dependencies.

Stacked on #144. The first commit here is that PR (the pure Service.dependencyLevels scheduler + its static tests). Please merge #144 first; this PR's second commit is the launch change that consumes it. Happy to rebase once #144 lands.

What changed

  • Concurrent waves. The serial for target in project.services { try await configService(...) } becomes: compute Service.dependencyLevels(...), then for each wave run its services in a withThrowingTaskGroup, awaiting the whole wave before the next. A dependency cycle throws at dependencyLevels before anything launches, so it can never deadlock.
  • LaunchState actor. Every piece of cross-service mutable state — container names, IPs, start states, health, console colors, and the IP-substituted environment — moves out of the value-type command and into an actor. Those dictionaries were previously mutated from the serial loop; launching a wave concurrently would race them, and the command's @unchecked Sendable hid that from the compiler. Routing all reads/writes through the actor makes concurrent launches data-race-free (compiles clean under -swift-version 6).
  • Semantics preserved by the wave barrier. Because a wave fully completes before the next begins, a service's dependencies have always reached their state before it starts, so depends_on ordering and the service_started / service_healthy / service_completed_successfully conditions behave exactly as before. Foreground mode, one-shot exit-code handling, and the SIGINT/SIGTERM handler (installed after the group returns) are unchanged.
  • --sequential opt-out. Restores the original one-at-a-time behavior.

Verification

The pure scheduler is unit-tested (in #144). The launch path needs the daemon, so it was verified end-to-end against Apple container:

scenario result
8 independent services, --sequential 11.6s, 8/8 running
8 independent services, parallel (default) 6.1s, 8/8 running (~1.9×)
web → app → db chain, parallel exit 0, comes up strictly db → app → web, 3/3 running

Full static suite stays green:

swift test --filter Container_Compose_StaticTests
✔ Test run with 242 tests in 22 suites passed

Notes / scope

  • Within a wave, services are independent by construction, so cross-service reads (peer IPs for env substitution) only ever read already-completed earlier waves.
  • The /etc/hosts cross-patch fallback (DNS-unavailable path) stays best-effort as before; its append is idempotent (grep-guarded).
  • --no-deps is not affected (it doesn't exist today; selection still pulls transitive deps).

Groups services into launch WAVES by their depends_on graph: every service
in a wave has all of its in-set dependencies in an earlier wave, so a whole
wave can be launched concurrently. This is the pure, daemon-free core of
parallelizing `up` (Mcrich23#128), where independent services currently start
sequentially (~23s for 11 independent services vs ~7s in parallel).

- depends_on entries naming a service outside the selected set (e.g.
  profile-excluded) are ignored, matching topoSortConfiguredServices.
- Throws on a dependency cycle (including a self-dependency) before
  returning, so a cyclic graph can never deadlock a concurrent launch.

Pure static function mirroring the existing graph helpers, with 6 static
tests: linear chain, diamond, independent set, cycle, self-dependency, and
out-of-set dependency. No behavior change yet — `up` still launches serially;
this lands the schedulable core first.
`up` launched every service strictly one at a time, so N independent
services paid N× the per-service start + readiness cost (the issue reports
~23s for 11 independent services). It now launches each dependency WAVE
(from `Service.dependencyLevels`) concurrently: services with no pending
dependency start together, while a dependent still waits for its
dependencies to reach their required condition.

All cross-service launch state — container names, IPs, start states,
health, console colors, and the IP-substituted environment — moves into a
new `LaunchState` actor so concurrent launches are data-race-free. Those
dictionaries were previously stored on the value-type command and mutated
from a serial loop, and `@unchecked Sendable` hid that hazard. A barrier
between waves guarantees a service's dependencies are fully started before
it begins, so `depends_on` ordering and conditions (service_started /
service_healthy / service_completed_successfully) are preserved.

The new `--sequential` flag restores the original one-at-a-time behavior.

Verified on the Apple `container` daemon: 8 independent services took
11.6s sequential vs 6.1s parallel (all reaching running); a web->app->db
chain still comes up strictly in dependency order.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Containers launch sequentially - can they be launched concurrently?

1 participant