feat: launch independent services concurrently in up (#128) - #145
Open
OrtegaMatias wants to merge 2 commits into
Open
feat: launch independent services concurrently in up (#128)#145OrtegaMatias wants to merge 2 commits into
up (#128)#145OrtegaMatias wants to merge 2 commits into
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #128:
uplaunched 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.What changed
for target in project.services { try await configService(...) }becomes: computeService.dependencyLevels(...), then for each wave run its services in awithThrowingTaskGroup, awaiting the whole wave before the next. A dependency cycle throws atdependencyLevelsbefore anything launches, so it can never deadlock.LaunchStateactor. 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 Sendablehid that from the compiler. Routing all reads/writes through the actor makes concurrent launches data-race-free (compiles clean under-swift-version 6).depends_onordering and theservice_started/service_healthy/service_completed_successfullyconditions behave exactly as before. Foreground mode, one-shot exit-code handling, and the SIGINT/SIGTERM handler (installed after the group returns) are unchanged.--sequentialopt-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:--sequentialweb → app → dbchain, parallelFull static suite stays green:
Notes / scope
/etc/hostscross-patch fallback (DNS-unavailable path) stays best-effort as before; its append is idempotent (grep-guarded).--no-depsis not affected (it doesn't exist today; selection still pulls transitive deps).