For vm4006: one hsadmin-ng backend build next to a frontend build, never two backend builds. - New instance-level key `executor.maxConcurrentPerRepository` (unset = only the global cap). - A build takes its repository slot before a global one, so a waiting build never blocks another repository. - Sized once like `maxConcurrent`; a change needs a restart. PR-doc: `docs/prs/2026-10-03-PR#103-max-concurrent-per-repository.md` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: mhoennig <michael@hoennig.de> Reviewed-on: #103
60 lines
3.1 KiB
Markdown
60 lines
3.1 KiB
Markdown
> **WARNING:** This document describes only the change applied in this PR.
|
|
> It may already be outdated once the next PR is merged.
|
|
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
|
|
|
|
## The Problem
|
|
|
|
vm4006 builds hsadmin-ng (backend) and hsadmin-ng.frontend.
|
|
The machine can take one backend build next to a frontend build, but not two backend builds.
|
|
Werkator knew two limits: `executor.maxConcurrent` over all repositories, and one build per repository and branch.
|
|
With `maxConcurrent: 2`, two backend branches could build at the same time; with `maxConcurrent: 1`, the frontend waited behind every backend build.
|
|
|
|
## Non-Goals
|
|
|
|
- No limit per registry entry: one value for all repositories covers the case; an exception per entry can follow when someone needs it.
|
|
- No build-definition key: what a machine can take is the host's decision, so ADR 0010 does not apply.
|
|
|
|
## The Scenarios
|
|
|
|
### Feature: A cap per repository below the global one
|
|
|
|
#### Scenario#103.01: A second build of the same repository waits, another repository's runs
|
|
|
|
So that a heavy repository never builds twice at once, and a light one is not held up by it.
|
|
|
|
- **Given** `maxConcurrent: 2` and `maxConcurrentPerRepository: 1`
|
|
- **And** a running build of one repository
|
|
- **When** a second build of that repository and then a build of another repository are queued
|
|
- **Then** the other repository's build takes the second slot, although it was queued last
|
|
- **And** the second build of the first repository starts once its repository's slot is free
|
|
|
|
##### Verified by
|
|
|
|
- [BuildExecutorIntegrationTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildExecutorIntegrationTest.kt) — Scenario#103.01
|
|
- Without the key nothing changes: [BuildExecutorIntegrationTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildExecutorIntegrationTest.kt) — "builds of different branches run concurrently"
|
|
|
|
#### Scenario#103.02: Unset by default, read like the rest of the section
|
|
|
|
So that every existing instance keeps its behaviour.
|
|
|
|
- **Given** a configuration without and with `executor.maxConcurrentPerRepository`
|
|
- **When** it is loaded
|
|
- **Then** it is unset, or the value given
|
|
|
|
##### Verified by
|
|
|
|
- [ConfigLoaderUnitTest](../../src/test/kotlin/de/hoennig/werkator/config/ConfigLoaderUnitTest.kt) — Scenario#103.02
|
|
|
|
## The Solution
|
|
|
|
- `ExecutorConfig.maxConcurrentPerRepository`, instance-level like the whole `executor` section, so no branch can raise it.
|
|
- `BuildExecutor` sizes it once with the global semaphore and keeps one semaphore per `RepoContext`.
|
|
- A build takes its repository's slot first and the global one second: a build waiting for its repository never sits on a global slot that another repository's build could use.
|
|
A build cancelled while it waits for its repository's slot gives it back without waiting for a global one.
|
|
- Like `maxConcurrent`, a change takes effect with the next restart.
|
|
|
|
## Additional Changes
|
|
|
|
- `docs/configuration.md`, `docs/deployment.md`, `docs/llms.md`, the `init` template, `AGENTS.md` and the architecture skill describe the key.
|
|
- Version 1.34.0 with its release note.
|