Skip to content

Commit c19e979

Browse files
Merge pull request #818 from gastownhall/worktree-fix-772-wake-budget
config: make wake-per-tick budget configurable via [daemon] (#772)
2 parents 6a63a73 + c6528d6 commit c19e979

8 files changed

Lines changed: 661 additions & 6 deletions

File tree

cmd/gc/session_lifecycle_parallel.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -788,6 +788,7 @@ func executePlannedStartsTraced(
788788
if len(candidates) == 0 {
789789
return 0
790790
}
791+
maxWakes := cfg.Daemon.MaxWakesPerTickOrDefault()
791792
waveByCandidate, ok := candidateWaveOrder(candidates, cfg, desiredState, sp, cityName, store)
792793
if !ok {
793794
fmt.Fprintln(stderr, "session reconciler: dependency graph fallback to serial start order") //nolint:errcheck
@@ -810,7 +811,7 @@ func executePlannedStartsTraced(
810811
if len(waveCandidates) == 0 {
811812
continue
812813
}
813-
if wakeCount >= defaultMaxWakesPerTick {
814+
if wakeCount >= maxWakes {
814815
for _, candidate := range waveCandidates {
815816
logLifecycleOutcome(stderr, "start", wave, candidate.name(), candidate.logicalTemplate(cfg), "deferred_by_wake_budget", time.Time{}, time.Time{}, nil)
816817
}
@@ -825,13 +826,13 @@ func executePlannedStartsTraced(
825826
ready = append(ready, candidate)
826827
}
827828
for offset := 0; offset < len(ready); {
828-
if wakeCount >= defaultMaxWakesPerTick {
829+
if wakeCount >= maxWakes {
829830
for _, candidate := range ready[offset:] {
830831
logLifecycleOutcome(stderr, "start", wave, candidate.name(), candidate.logicalTemplate(cfg), "deferred_by_wake_budget", time.Time{}, time.Time{}, nil)
831832
}
832833
break
833834
}
834-
batchSize := min(defaultMaxParallelStartsPerWave, defaultMaxWakesPerTick-wakeCount)
835+
batchSize := min(defaultMaxParallelStartsPerWave, maxWakes-wakeCount)
835836
end := min(offset+batchSize, len(ready))
836837
var prepared []preparedStart
837838
for _, candidate := range ready[offset:end] {

cmd/gc/session_lifecycle_parallel_test.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,55 @@ func TestReconcileSessionBeads_BlockedCandidatesDoNotConsumeWakeBudget(t *testin
741741
}
742742
}
743743

744+
// TestReconcileSessionBeads_DaemonMaxWakesPerTickOverride covers the fix for
745+
// issue #772: [daemon].max_wakes_per_tick = N raises (or lowers) the wake
746+
// budget away from the 5-per-tick default. Cities with slow cold-starts
747+
// need this to drain the candidate queue.
748+
func TestReconcileSessionBeads_DaemonMaxWakesPerTickOverride(t *testing.T) {
749+
env := newReconcilerTestEnv()
750+
override := 8
751+
env.cfg = &config.City{
752+
Daemon: config.DaemonConfig{MaxWakesPerTick: &override},
753+
Agents: []config.Agent{
754+
{Name: "ready-1"},
755+
{Name: "ready-2"},
756+
{Name: "ready-3"},
757+
{Name: "ready-4"},
758+
{Name: "ready-5"},
759+
{Name: "ready-6"},
760+
{Name: "ready-7"},
761+
{Name: "ready-8"},
762+
{Name: "ready-9"},
763+
},
764+
}
765+
names := []string{"ready-1", "ready-2", "ready-3", "ready-4", "ready-5", "ready-6", "ready-7", "ready-8", "ready-9"}
766+
for _, name := range names {
767+
env.addDesired(name, name, false)
768+
}
769+
770+
var seeded []beads.Bead
771+
for _, name := range names {
772+
b := env.createSessionBead(name, name)
773+
env.markSessionCreating(&b)
774+
seeded = append(seeded, b)
775+
}
776+
777+
woken := env.reconcile(seeded)
778+
779+
if woken != override {
780+
t.Fatalf("woken = %d, want %d (overridden wake budget)", woken, override)
781+
}
782+
// First 8 run, 9th is deferred_by_wake_budget.
783+
for _, name := range names[:override] {
784+
if !env.sp.IsRunning(name) {
785+
t.Fatalf("%s should have started under override=%d", name, override)
786+
}
787+
}
788+
if env.sp.IsRunning(names[override]) {
789+
t.Fatalf("%s should have been deferred once budget hit", names[override])
790+
}
791+
}
792+
744793
func TestPrepareStartCandidate_NoneModeInitialMessageStaysInNudge(t *testing.T) {
745794
store := beads.NewMemStore()
746795
bead, err := store.Create(beads.Bead{

cmd/gc/session_types.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package main
33
import (
44
"sync"
55
"time"
6+
7+
"github.com/gastownhall/gascity/internal/config"
68
)
79

810
// WakeReason describes why a session should be awake.
@@ -185,9 +187,11 @@ const (
185187
// before it's considered stable (not a rapid exit / crash).
186188
stabilityThreshold = 30 * time.Second
187189

188-
// maxWakesPerTick limits how many sessions can be woken per reconciler
189-
// tick to prevent thundering herd after controller restart.
190-
defaultMaxWakesPerTick = 5
190+
// defaultMaxWakesPerTick mirrors config.DefaultMaxWakesPerTick (kept
191+
// here so tests and non-config call sites don't need to take a
192+
// dependency on internal/config just for the default). Configurable
193+
// per city via [daemon].max_wakes_per_tick; see issue #772.
194+
defaultMaxWakesPerTick = config.DefaultMaxWakesPerTick
191195

192196
// defaultTickBudget is the wall-clock budget per reconciler tick.
193197
// Remaining work is deferred to the next tick.

0 commit comments

Comments
 (0)