Add a random churn/soak scenario mode (neutron chaos)#16
Merged
Conversation
The chaos churn engine needs the same per-operation retry/backoff policy that apply uses and the same latency-percentile computation the metrics aggregate uses. Export both so the engine reuses them instead of duplicating the transient/conflict/quota backoff and the percentile math. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
The generic Delete cannot remove a router interface (interfaces are detached, not deleted), and the existing DetachRouterInterfaces removes all of a router's interfaces at once. The chaos churn engine needs to delete one interface at a time, so add RemoveRouterInterface mirroring CreateRouterInterface: exactly one of subnetID or portID identifies the interface, the call is timed under KindRouterInterface, and the wrapped error stays IsNotFound-classifiable so a repeat detach reads as success. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
Scenarios gain an optional chaos block configuring the churn/soak mode: duration, a random interval range, a parallelism cap, and the churn-ratio/target-fill controller knobs. The block is a pointer so an absent block stays nil and apply/generate ignore it. A custom Duration YAML type decodes Go duration strings (30m, 200ms) under strict YAML, which yaml.v2 does not do natively. Validation runs only when the block is present and does not require duration, since a CLI flag may supply it. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
Build turns a plan into a churn graph: every plan resource becomes one node whose Parents are resolved exactly as executor.Apply resolves references, so the engine can later create only nodes whose parents exist and delete only nodes whose dependents are gone. Each node binds per-kind create/delete closures over the typed Neutron wrappers (delete uses Delete for every kind except router interfaces, which use RemoveRouterInterface). Floating IPs are omitted when no external network is available, mirroring apply. No scheduling yet. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
A single scheduler goroutine draws every churn decision from a seeded RNG: a random inter-tick delay, a random fan-out, and a create-vs-delete pick biased by the churn-ratio/target-fill controller and constrained to the currently valid candidates (absent nodes whose parents are present; present nodes whose dependents are absent). Decisions are deterministic for a given seed and config; the resulting create/delete operations run concurrently through a bounded semaphore with executor.WithRetry, each waiting on its dependency operations before acquiring a slot so the pool cannot deadlock and no operation violates the dependency graph. A clock seam keeps the determinism test off the wall clock. The engine reports the decision log, churn counters, population series, the final live set, and per-time-bucket latency/error stats. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
The run record gains an optional Chaos field holding a churn run's create/delete and cycle counts, live-population summary, target fill, and per-time-bucket latency/error statistics. The schema lives in the run package so report and status render it without importing the chaos engine. WriteTable appends a churn summary and a per-bucket table when the field is set; WriteJSON nests the chaos object under a chaos key. An apply record (Chaos nil) renders byte-identically to before on both paths. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
neutron chaos runs the random churn/soak engine: it merges the chaos config from defaults, the scenario chaos block, and flags (flags override), validates the merged config before any cloud call, pre-checks quota against the full plan, runs the seeded churn for the duration, and records the run with its churn stats. On a clean finish it tears the topology down by tag and reports a leak check; an interrupt or --no-cleanup leaves the resources in place and prints the cleanup hint. buildPlanFromFlags now also returns the parsed scenario so the command can read its chaos block; apply and generate ignore the new return. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
Document neutron chaos across the README: the subcommand and its flags in the CLI design section, the chaos: scenario block in the parametrization section, the churn-specific stats and end-of-run leak check in the metrics section, the deterministic scheduler / envelope ceiling / controller / teardown policy in the execution model, the new internal/chaos package in the layout, and a Phase 1 roadmap entry. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
berendt
force-pushed
the
implement/issue-15-chaos-churn-mode
branch
from
June 24, 2026 19:29
412f985 to
4aacde0
Compare
berendt
marked this pull request as ready for review
June 24, 2026 19:31
The neutron chaos subcommand requires a churn duration with no built-in default, so the small/medium/large profiles — which carried no chaos block — failed before any work with "chaos duration must be set". Add a self-contained chaos block (duration, intervals, fan-out, controller knobs) to each profile, scaled by size, so `neutron chaos --scenario scenarios/<profile>.yaml` runs straight away with no flags. CLI flags still override the block; generate/apply ignore it, so the golden plan is unchanged. Keep the shared scenario fixtures chaos-free and split the chaos block out of the two profile-matching equality tests (Scenario.Chaos is a pointer, so == never matches two non-nil blocks). Add TestProfilesShipRunnableChaosBlock and a cmd-level test that runs each shipped profile through chaos without --duration. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
These commands were silent between their start and their final summary. Add an info-level line per operation — each created resource, each scheduled churn create/delete, each teardown delete — at one hook per command (executor.provision, engine.step, executor.deleteResources), so every line is emitted once per logical op rather than per retry. On top of that, run a periodic heartbeat (metrics.Collector.Snapshot + the startHeartbeat helper) reporting cumulative ops, the rate since the last tick, the ok/failed split, and elapsed time. All output is on stderr at info, so --log-level warn silences it while the stdout metrics summary and run-record path are unchanged. The executor and chaos test packages get a TestMain that discards logs so the new per-op lines do not flood test output. Assisted-by: Claude:claude-opus-4-8 Signed-off-by: Christian Berendt <berendt@b42labs.com>
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.
Closes #15
Summary
Adds a random churn/soak mode for Neutron — a new
neutron chaossubcommand that, instead of building a topology once, runs for a configured duration and uses the scenario plan as the upper-bound envelope of what may exist at any moment. For the whole runtime it continuously creates and deletes resources at random, configurable intervals and parallelism, keeping the cloud under fluctuating churn. The schedule is fully seeded and reproducible: the samescenario + seed + duration + interval/parallelismproduce the identical create/delete sequence.The implementation reuses Phase 1 machinery (plan, neutron wrappers + run tagging, executor pool/backoff, metrics, run records, tag-based cleanup + quota pre-check) rather than duplicating it.
Change set (in commit order)
05e1c14Exportexecutor.WithRetryandmetrics.ComputeLatencyfor reuse — exports the existing per-operation retry/backoff policy and latency-percentile computation so the churn engine reuses them instead of re-implementing the transient/conflict/quota backoff and percentile math.d8cba54Add neutron per-interface router-interface removal wrapper — addsRemoveRouterInterface, mirroringCreateRouterInterface(exactly one of subnetID/portID), because the genericDeletecannot detach an interface and the existingDetachRouterInterfacesremoves them all at once. The engine needs to delete one interface at a time; the call is timed underKindRouterInterfaceand staysIsNotFound-classifiable so a repeat detach reads as success.04bcaedAdd chaos config block to the scenario format — an optionalchaos:block (duration, random interval range, parallelism cap, churn-ratio/target-fill knobs). It is a pointer so an absent block staysnilandapply/generateignore it. A customDurationYAML type decodes Go duration strings (30m,200ms) under strict YAML, which yaml.v2 does not do natively. Validation runs only when the block is present and does not requireduration(a CLI flag may supply it).d50dd75Addinternal/chaosplan dependency graph and node model —Buildturns a plan into a churn graph: each plan resource becomes a node whoseParentsare resolved exactly asexecutor.Applyresolves references, so the engine can later create only nodes whose parents exist and delete only nodes whose dependents are gone. Each node binds per-kind create/delete closures over the typed Neutron wrappers (delete usesDeletefor every kind except router interfaces, which useRemoveRouterInterface). Floating IPs are omitted when no external network is available, mirroringapply. No scheduling yet.aeb9351Add deterministic chaos engine, scheduler, and clock seam — a single scheduler goroutine draws every churn decision from a seeded RNG (random inter-tick delay, random fan-out, create-vs-delete pick biased by the churn-ratio/target-fill controller and constrained to currently valid candidates). Operations run concurrently through a bounded semaphore withexecutor.WithRetry, each waiting on its dependency operations before acquiring a slot so the pool cannot deadlock and no operation violates the dependency graph. A clock seam keeps the determinism test off the wall clock. The engine reports the decision log, churn counters, population series, the final live set, and per-time-bucket latency/error stats.efd20f6Add chaos stats to run record and report rendering — the run record gains an optionalChaosfield (create/delete and cycle counts, live-population summary, target fill, per-bucket latency/error stats). The schema lives in therunpackage soreport/statusrender it without importing the chaos engine.WriteTableappends a churn summary and per-bucket table when set;WriteJSONnests it under achaoskey. Anapplyrecord (Chaosnil) renders byte-identically to before.26508bbAdd neutron chaos subcommand wired to the chaos engine —neutron chaosmerges chaos config from defaults, the scenariochaos:block, and flags (flags override), validates the merged config before any cloud call, pre-checks quota against the full plan, runs the seeded churn for the duration, and records the run with its churn stats. On a clean finish it tears the topology down by tag and reports a leak check; an interrupt or--no-cleanupleaves resources in place and prints the cleanup hint.buildPlanFromFlagsnow also returns the parsed scenario (apply/generate ignore the new return).412f985Document the chaos churn mode in the README — documents the subcommand and flags, thechaos:scenario block, the churn stats and leak check, the deterministic scheduler / envelope ceiling / controller / teardown policy, the new package in the layout, and a Phase 1 roadmap entry.Decisions taken against the issue's open questions
The issue left several decisions open; this branch resolves them as:
chaos(issue listedsoak/churn/random/chaos). The scenario block ischaos:rather than the issue's examplesoak:.chaos:scenario block and CLI flags, with flags overriding.churn_ratioand atarget_fillcontroller.cleanup --run, but listed auto-cleanup as a valid alternative).--no-cleanupand interrupts leave resources in place with a cleanup hint, matching the safe path.PrecheckQuotaruns against the full plan up front, since the envelope is the worst case.Reviewer note
The working tree has uncommitted changes to
internal/chaos/engine.goandinternal/chaos/engine_test.go(amissingParentIDhelper, alaunchsignature takingctx/offset, and validation tweaks) that are not part of any commit and therefore not in this PR. This PR reflects only the committed state; those changes were left untouched by this publish step.Implemented by planwerk-review 495ffd8-dirty with Claude:claude-opus-4-8