controller v0.269.0: whole restore from the second drive; crash loops stopped; exact image digests; steps judged by their own .felhom.yml (decisions 26-28, R-661 R-666 R-667 R-668 R-664 R-665 R-662, 09 6.4 part 6)
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -295,6 +295,9 @@ func fillHoldReason(g UpdateGuards, st *Stack) {
|
||||
if nw, ok := g.(holdWholeCopy); ok {
|
||||
st.HoldNoWholeCopy = nw.HoldNoWholeCopy(st.Name)
|
||||
}
|
||||
if hk, ok := g.(holdKinder); ok {
|
||||
st.HoldKind = hk.HoldKind(st.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
// R-480: an update that ended HELD carries the hold's sentence as its UpdateError. Once that hold
|
||||
@@ -307,6 +310,11 @@ func fillHoldReason(g UpdateGuards, st *Stack) {
|
||||
}
|
||||
}
|
||||
|
||||
// holdKinder is the OPTIONAL half of UpdateGuards that names the hold's kind (v0.269.0).
|
||||
type holdKinder interface {
|
||||
HoldKind(name string) string
|
||||
}
|
||||
|
||||
// holdWholeCopy is the OPTIONAL half of UpdateGuards that says a hold names no copy (R-659).
|
||||
type holdWholeCopy interface {
|
||||
HoldNoWholeCopy(name string) bool
|
||||
@@ -445,6 +453,14 @@ func (m *Manager) updateMemoryRefusal(name string, st *Stack) *UpdateRefusal {
|
||||
return nil
|
||||
}
|
||||
newMeta := LoadMetadata(filepath.Dir(catPath))
|
||||
// R-664 (v0.269.0): on a ladder the NEXT STEP's own memory request is what this press installs.
|
||||
if st.AppConfig != nil && len(st.AppConfig.PinnedImages) > 0 {
|
||||
if step, err := nextLadderStep(filepath.Dir(catPath), st.AppConfig.PinnedImages); err == nil && step.Meta != catPath {
|
||||
if mm, merr := loadMetadataFile(step.Meta); merr == nil {
|
||||
newMeta = mm
|
||||
}
|
||||
}
|
||||
}
|
||||
newReq, newLim := ParseMemoryMB(newMeta.Resources.MemRequest), ParseMemoryMB(newMeta.Resources.MemLimit)
|
||||
if newReq == 0 {
|
||||
m.logger.Printf("[WARN] [stacks] update %s: the new template declares no memory request — proceeding without the memory check", name)
|
||||
@@ -657,9 +673,32 @@ func (m *Manager) updateCompose(dir string, env []string, args ...string) (strin
|
||||
}
|
||||
|
||||
func (m *Manager) updateHealth(ctx context.Context, name string, timeout time.Duration) (bool, string) {
|
||||
if m.updateHealthFn != nil {
|
||||
return m.updateHealthFor(ctx, name, timeout, "")
|
||||
}
|
||||
|
||||
// updateHealthFor is the verify with the NEW version's own .felhom.yml (R-665/R-664): metaFile is the
|
||||
// catalog's (or the step's) file journaled at the start of the job; "" = the stack dir's (an update
|
||||
// journaled by an older controller).
|
||||
func (m *Manager) updateHealthFor(ctx context.Context, name string, timeout time.Duration, metaFile string) (bool, string) {
|
||||
if m.updateHealthFn != nil && m.updateHealthMetaFn == nil {
|
||||
return m.updateHealthFn(ctx, name, timeout)
|
||||
}
|
||||
if metaFile != "" {
|
||||
if _, err := os.Stat(metaFile); err == nil {
|
||||
meta := LoadMetadata(filepath.Dir(metaFile))
|
||||
if filepath.Base(metaFile) != ".felhom.yml" {
|
||||
if mm, err := loadMetadataFile(metaFile); err == nil {
|
||||
meta = mm
|
||||
}
|
||||
}
|
||||
m.lastVerifyMeta = metaFile
|
||||
if m.updateHealthMetaFn != nil {
|
||||
return m.updateHealthMetaFn(ctx, name, timeout, &meta)
|
||||
}
|
||||
return m.waitUpdateHealthyMeta(ctx, name, timeout, &meta)
|
||||
}
|
||||
m.logger.Printf("[WARN] [stacks] update %s: the new version's .felhom.yml %s is gone — judging with the stack dir's", name, metaFile)
|
||||
}
|
||||
return m.waitUpdateHealthy(ctx, name, timeout)
|
||||
}
|
||||
|
||||
@@ -712,15 +751,20 @@ func (m *Manager) runGuardedUpdate(ctx context.Context, name string) {
|
||||
// first, before anything moves, so a step the catalog promises and does not carry refuses here
|
||||
// rather than jumping past it. An unpinned app is left to today's behaviour (advancePinTo no-ops).
|
||||
stepSrc := m.CatalogTemplatePath(name, "docker-compose.yml")
|
||||
stepMeta := m.CatalogTemplatePath(name, ".felhom.yml")
|
||||
if cfg := LoadAppConfig(dir); cfg != nil && len(cfg.PinnedImages) > 0 {
|
||||
step, serr := nextLadderStep(filepath.Dir(stepSrc), cfg.PinnedImages)
|
||||
if serr != nil {
|
||||
fail("update.error.pin_failed", "update ladder: "+serr.Error())
|
||||
return
|
||||
}
|
||||
stepSrc = step.Source
|
||||
stepSrc, stepMeta = step.Source, step.Meta
|
||||
m.logger.Printf("[INFO] [stacks] update %s: ladder — %s", name, step.Why)
|
||||
}
|
||||
// R-665 (v0.269.0): the new version is judged by ITS OWN .felhom.yml, journaled so a resumed verify
|
||||
// uses it too — never by the stack dir's, which a restore rewrites with the unit's older file until
|
||||
// the next catalog sync (measured on 9202 2026-09-24: a failing edge passed on the restored probe).
|
||||
entry.NewMeta = stepMeta
|
||||
// R-475: the precondition is a copy on ANY tier, chosen in the order 2, 1, 3, and the age rule
|
||||
// applies to whichever tier is chosen. The first FRESH copy wins — not merely the first copy — so a
|
||||
// stale second-drive mirror never forces a backup while the app's own unit is minutes old.
|
||||
@@ -813,7 +857,7 @@ func (m *Manager) runGuardedUpdate(ctx context.Context, name string) {
|
||||
fail("update.error.journal_failed", "journal write failed")
|
||||
return
|
||||
}
|
||||
if err := m.advancePinTo(name, dir, stepSrc); err != nil {
|
||||
if err := m.advancePinTo(name, dir, stepSrc, stepMeta); err != nil {
|
||||
m.pinBack(name, dir, entry)
|
||||
fail("update.error.pin_failed", "advancing the pin: "+err.Error())
|
||||
return
|
||||
@@ -876,7 +920,7 @@ func (m *Manager) verifyAndConclude(ctx context.Context, name, dir string, env [
|
||||
}
|
||||
timeout := m.healthTimeout()
|
||||
waitStart := m.now()
|
||||
healthy, detail := m.updateHealth(ctx, name, timeout)
|
||||
healthy, detail := m.updateHealthFor(ctx, name, timeout, entry.NewMeta)
|
||||
if !healthy {
|
||||
m.failAndHold(ctx, name, dir, env, rp, "not healthy: "+detail, entry)
|
||||
return
|
||||
@@ -1131,6 +1175,9 @@ type updateJournalEntry struct {
|
||||
Copied bool `json:"copied,omitempty"`
|
||||
PrevMeta string `json:"prev_meta,omitempty"`
|
||||
NewPin map[string]string `json:"new_pin,omitempty"`
|
||||
// NewMeta (v0.269.0, R-665/R-664) is the NEW version's own .felhom.yml — the catalog's, or the
|
||||
// ladder step's — used for the verify, so a resumed verify judges by the same file.
|
||||
NewMeta string `json:"new_meta,omitempty"`
|
||||
}
|
||||
|
||||
type updateJournal struct {
|
||||
|
||||
Reference in New Issue
Block a user