From cff5b56fb7492a3a5222c45dcdd6322b45de7353 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Tue, 5 May 2026 13:43:23 +0400 Subject: [PATCH 1/2] fix(buildernet): make --rbuilder flag work Buildernet removed beacon_healthmon but left beacon's health-check sidecar label pointing at it, so manifest validation failed when rbuilder declared DependsOnHealthy("beacon"). Beacon also cannot reach a healthy state in buildernet until a builder connects (--target-peers=1), so depending on beacon health would deadlock anyway. Strip the stale label after removing the sidecar, and downgrade rbuilder's beacon dependency from healthy to running when used inside buildernet. Co-Authored-By: Claude Opus 4.7 (1M context) --- playground/recipe_buildernet.go | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/playground/recipe_buildernet.go b/playground/recipe_buildernet.go index 9d5b77b..ba34536 100644 --- a/playground/recipe_buildernet.go +++ b/playground/recipe_buildernet.go @@ -45,7 +45,8 @@ func (b *BuilderNetRecipe) Apply(ctx *ExContext) *Component { // We need these for letting the builder connect to the beacon node. // Basically, the beacon node can never be healthy until the builder // connects. - if beacon := component.FindService("beacon"); beacon != nil { + beacon := component.FindService("beacon") + if beacon != nil { beacon.ReplaceArgs(map[string]string{ "--target-peers": "1", }) @@ -56,6 +57,19 @@ func (b *BuilderNetRecipe) Apply(ctx *ExContext) *Component { } // Remove beacon healthmon - doesn't work with --target-peers=1 which is required for builder VM component.RemoveService("beacon_healthmon") + if beacon != nil { + delete(beacon.Labels, healthCheckSidecarLabel) + } + + // Beacon never reaches healthy state until a builder connects (target-peers=1), + // so any builder added by the L1 recipe must wait on beacon running, not healthy. + if rbuilder := component.FindService("rbuilder"); rbuilder != nil { + for _, dep := range rbuilder.DependsOn { + if dep.Name == "beacon" && dep.Condition == DependsOnConditionHealthy { + dep.Condition = DependsOnConditionRunning + } + } + } component.RunContenderIfEnabled(ctx) From fbc2a1b3e0bf63c6fa57d5dbc6be48fa77d249f5 Mon Sep 17 00:00:00 2001 From: Solar Mithril Date: Tue, 5 May 2026 13:50:10 +0400 Subject: [PATCH 2/2] refactor(buildernet): generalize beacon-dep handling, drop double contender MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review feedback on the previous fix: - RemoveService now strips dangling health-check-sidecar label refs across the component tree, so callers can no longer leave a beacon pointing at a removed healthmon. - Add Component.WalkServices and Service.ReplaceDependency as small reusable primitives. - Buildernet uses WalkServices to downgrade *any* DependsOnHealthy("beacon") to running, not just rbuilder. This also covers contender, which has the same dependency and would have hit the same validation failure. - Remove the duplicate RunContenderIfEnabled call in buildernet — L1Recipe already adds contender, so calling it again at the outer level was registering the service twice. Co-Authored-By: Claude Opus 4.7 (1M context) --- playground/manifest.go | 51 +++++++++++++++++++++++++++++++-- playground/recipe_buildernet.go | 30 +++++++------------ 2 files changed, 59 insertions(+), 22 deletions(-) diff --git a/playground/manifest.go b/playground/manifest.go index f0732f5..021216b 100644 --- a/playground/manifest.go +++ b/playground/manifest.go @@ -87,6 +87,16 @@ func (p *Component) AddService(ctx *ExContext, srv ComponentGen) { p.Inner = append(p.Inner, srv.Apply(ctx)) } +// WalkServices invokes fn for every service in the component tree. +func (p *Component) WalkServices(fn func(*Service)) { + for _, svc := range p.Services { + fn(svc) + } + for _, inner := range p.Inner { + inner.WalkServices(fn) + } +} + // FindService finds a service by name in the component tree func (p *Component) FindService(name string) *Service { for _, svc := range p.Services { @@ -102,16 +112,37 @@ func (p *Component) FindService(name string) *Service { return nil } -// RemoveService removes a service by name from the component tree +// RemoveService removes a service by name from the component tree. +// It also clears any health-check-sidecar labels that reference the removed +// service so manifest validation does not fail on a dangling sidecar pointer. func (p *Component) RemoveService(name string) { + p.removeServiceRecursive(name) + p.clearSidecarLabel(name) +} + +func (p *Component) removeServiceRecursive(name string) bool { for i, svc := range p.Services { if svc.Name == name { p.Services = append(p.Services[:i], p.Services[i+1:]...) - return + return true } } for _, inner := range p.Inner { - inner.RemoveService(name) + if inner.removeServiceRecursive(name) { + return true + } + } + return false +} + +func (p *Component) clearSidecarLabel(sidecarName string) { + for _, svc := range p.Services { + if svc.Labels[healthCheckSidecarLabel] == sidecarName { + delete(svc.Labels, healthCheckSidecarLabel) + } + } + for _, inner := range p.Inner { + inner.clearSidecarLabel(sidecarName) } } @@ -663,6 +694,20 @@ func (s *Service) DependsOnRunning(name string) *Service { return s } +// ReplaceDependency updates the condition of an existing depends-on edge to +// the named service. It is a no-op if no such edge exists. Useful when a +// downstream recipe knows that a target service can never reach the strict +// condition declared by an upstream component (e.g. a beacon that only goes +// healthy after its consumer connects). +func (s *Service) ReplaceDependency(name string, condition DependsOnCondition) *Service { + for _, dep := range s.DependsOn { + if dep.Name == name { + dep.Condition = condition + } + } + return s +} + func applyTemplate(templateStr string) (string, []Port, []NodeRef) { // TODO: Can we remove the return argument string? diff --git a/playground/recipe_buildernet.go b/playground/recipe_buildernet.go index ba34536..64a6e70 100644 --- a/playground/recipe_buildernet.go +++ b/playground/recipe_buildernet.go @@ -44,9 +44,9 @@ func (b *BuilderNetRecipe) Apply(ctx *ExContext) *Component { // Apply beacon service overrides for buildernet. // We need these for letting the builder connect to the beacon node. // Basically, the beacon node can never be healthy until the builder - // connects. - beacon := component.FindService("beacon") - if beacon != nil { + // connects, so we also drop its healthmon and downgrade any consumer's + // dependency on beacon from healthy to running. + if beacon := component.FindService("beacon"); beacon != nil { beacon.ReplaceArgs(map[string]string{ "--target-peers": "1", }) @@ -55,23 +55,15 @@ func (b *BuilderNetRecipe) Apply(ctx *ExContext) *Component { if mevBoostRelay := component.FindService("mev-boost-relay"); mevBoostRelay != nil { mevBoostRelay.DependsOnNone() } - // Remove beacon healthmon - doesn't work with --target-peers=1 which is required for builder VM component.RemoveService("beacon_healthmon") - if beacon != nil { - delete(beacon.Labels, healthCheckSidecarLabel) - } - - // Beacon never reaches healthy state until a builder connects (target-peers=1), - // so any builder added by the L1 recipe must wait on beacon running, not healthy. - if rbuilder := component.FindService("rbuilder"); rbuilder != nil { - for _, dep := range rbuilder.DependsOn { - if dep.Name == "beacon" && dep.Condition == DependsOnConditionHealthy { - dep.Condition = DependsOnConditionRunning - } - } - } - - component.RunContenderIfEnabled(ctx) + // Beacon never reaches healthy in buildernet, so any consumer that asked + // for healthy must accept running instead. + component.WalkServices(func(svc *Service) { + svc.ReplaceDependency("beacon", DependsOnConditionRunning) + }) + + // Note: contender is already added by L1Recipe.Apply, so we do not call + // RunContenderIfEnabled here. return component }