v0.276.0: a restore and a drive move keep the app's records (R-697, R-700)
gates / gates (push) Successful in 26s
gates / gates (push) Successful in 26s
A drive move persisted through the restore's fresh app.yaml write and dropped the pin: the syncer then copied the catalog verbatim and the next start jumped the app past its ladder (R-700). persistDriveFlip now changes HDD_PATH and nothing else. The restore's write carries the life records (conversion copies, desired_state, update history) from the app.yaml it replaces, and a second conversion no longer overwrites the first kept copy's record (R-697). 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:
@@ -590,11 +590,19 @@ func diffSnapshots(before, after []string) string {
|
||||
// ── keeping, then releasing, the old datadir's copy (B5) ────────────────────────────────────────
|
||||
|
||||
func (m *Manager) recordConversionCopy(name, dir string, cc *ConversionCopy) {
|
||||
set := func(cfg *AppConfig) {
|
||||
// R-697 (v0.276.0): a newer conversion never overwrites an older kept copy's record — the older one
|
||||
// moves to the earlier list and is released by the same rule.
|
||||
if cc != nil && cfg.ConversionCopy != nil && cfg.ConversionCopy.Copy != cc.Copy && !hasCopy(cfg.EarlierConversionCopies, cfg.ConversionCopy.Copy) {
|
||||
cfg.EarlierConversionCopies = append(cfg.EarlierConversionCopies, *cfg.ConversionCopy)
|
||||
}
|
||||
cfg.ConversionCopy = cc
|
||||
}
|
||||
cfg := LoadAppConfig(dir)
|
||||
if cfg == nil || (cc == nil && cfg.ConversionCopy == nil) {
|
||||
return
|
||||
}
|
||||
cfg.ConversionCopy = cc
|
||||
set(cfg)
|
||||
meta := LoadMetadata(dir)
|
||||
if err := SaveAppConfig(dir, cfg, m.encKey, SensitiveEnvVars(&meta)); err != nil {
|
||||
m.logger.Printf("[ERROR] [stacks] update %s: recording conversion_copy failed: %v", name, err)
|
||||
@@ -602,11 +610,38 @@ func (m *Manager) recordConversionCopy(name, dir string, cc *ConversionCopy) {
|
||||
}
|
||||
m.mu.Lock()
|
||||
if st, ok := m.stacks[name]; ok && st.AppConfig != nil {
|
||||
st.AppConfig.ConversionCopy = cc
|
||||
st.AppConfig.ConversionCopy = cfg.ConversionCopy
|
||||
st.AppConfig.EarlierConversionCopies = cfg.EarlierConversionCopies
|
||||
}
|
||||
m.mu.Unlock()
|
||||
}
|
||||
|
||||
func hasCopy(list []ConversionCopy, copyVol string) bool {
|
||||
for _, c := range list {
|
||||
if c.Copy == copyVol {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// forgetEarlierConversionCopy drops one released copy from the earlier list.
|
||||
func (m *Manager) forgetEarlierConversionCopy(name, dir, copyVol string) {
|
||||
m.mutateAppConfig(name, dir, "earlier_conversion_copies", func(cfg *AppConfig) bool {
|
||||
var keep []ConversionCopy
|
||||
for _, c := range cfg.EarlierConversionCopies {
|
||||
if c.Copy != copyVol {
|
||||
keep = append(keep, c)
|
||||
}
|
||||
}
|
||||
if len(keep) == len(cfg.EarlierConversionCopies) {
|
||||
return false
|
||||
}
|
||||
cfg.EarlierConversionCopies = keep
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// ReleaseConversionCopies removes each kept pre-conversion datadir copy whose app now has a backup
|
||||
// PROVEN after the conversion (any tier — the backup side returns only proven copies). Returns the
|
||||
// names released. Run periodically from main.go (TestConvert_ReleaseIsWiredAtStartup).
|
||||
@@ -617,43 +652,57 @@ func (m *Manager) ReleaseConversionCopies(ctx context.Context) []string {
|
||||
}
|
||||
var released []string
|
||||
for _, st := range m.GetStacks() {
|
||||
if st.AppConfig == nil || st.AppConfig.ConversionCopy == nil {
|
||||
if st.AppConfig == nil {
|
||||
continue
|
||||
}
|
||||
cc := st.AppConfig.ConversionCopy
|
||||
at, err := time.Parse(time.RFC3339, cc.At)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] [stacks] %s: conversion_copy has an unreadable time %q — the copy %s is kept", st.Name, cc.At, cc.Copy)
|
||||
continue
|
||||
}
|
||||
rp, ok, _ := g.RestorePoints(ctx, st.Name, func(p UpdateRestorePoint) bool { return p.ProvenAt.After(at) })
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
// v0.275.0 (A4): AND a dump the converted engine wrote. Without the stamps (older guards, or a unit
|
||||
// whose data is unstamped) the copy is KEPT — logged, retried at the next pass.
|
||||
src, hasStamps := g.(DumpStampSource)
|
||||
var dump DataDumpStamp
|
||||
if hasStamps {
|
||||
dump, ok = convertedDumpAt(src.DumpStamps(st.Name), cc, at)
|
||||
}
|
||||
if !hasStamps || !ok {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] %s: the pre-conversion copy %s is KEPT — a copy proven at %s exists, but no database dump written after %s by PostgreSQL %d is recorded yet", st.Name, cc.Copy, rp.ProvenAt.UTC().Format(time.RFC3339), cc.At, cc.To)
|
||||
// v0.276.0 (R-697): the current record and every earlier one a later conversion superseded.
|
||||
for _, e := range st.AppConfig.EarlierConversionCopies {
|
||||
e := e
|
||||
if m.releaseOneConversionCopy(ctx, g, st, &e) {
|
||||
m.forgetEarlierConversionCopy(st.Name, filepath.Dir(st.ComposePath), e.Copy)
|
||||
released = append(released, st.Name)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := m.copier().Remove(cc.Copy); err != nil {
|
||||
m.logger.Printf("[WARN] [stacks] %s: could not remove the pre-conversion copy %s: %v — kept, tried again later", st.Name, cc.Copy, err)
|
||||
continue
|
||||
if cc := st.AppConfig.ConversionCopy; cc != nil && m.releaseOneConversionCopy(ctx, g, st, cc) {
|
||||
m.recordConversionCopy(st.Name, filepath.Dir(st.ComposePath), nil)
|
||||
released = append(released, st.Name)
|
||||
}
|
||||
m.recordConversionCopy(st.Name, filepath.Dir(st.ComposePath), nil)
|
||||
m.logger.Printf("[INFO] [stacks] %s: REMOVED the pre-conversion datadir copy %s (PostgreSQL %d) — the converted app has a backup proven on %d: %s at %s, its dump %s written %s by %v", st.Name, cc.Copy, cc.From, cc.To, updateTierName(rp.Tier), rp.ProvenAt.UTC().Format(time.RFC3339), dump.File, dump.At.UTC().Format(time.RFC3339), dump.Images)
|
||||
released = append(released, st.Name)
|
||||
}
|
||||
return released
|
||||
}
|
||||
|
||||
// releaseOneConversionCopy removes one kept copy when its release conditions hold; true = removed.
|
||||
func (m *Manager) releaseOneConversionCopy(ctx context.Context, g UpdateGuards, st Stack, cc *ConversionCopy) bool {
|
||||
at, err := time.Parse(time.RFC3339, cc.At)
|
||||
if err != nil {
|
||||
m.logger.Printf("[WARN] [stacks] %s: conversion_copy has an unreadable time %q — the copy %s is kept", st.Name, cc.At, cc.Copy)
|
||||
return false
|
||||
}
|
||||
rp, ok, _ := g.RestorePoints(ctx, st.Name, func(p UpdateRestorePoint) bool { return p.ProvenAt.After(at) })
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
// v0.275.0 (A4): AND a dump the converted engine wrote. Without the stamps (older guards, or a unit
|
||||
// whose data is unstamped) the copy is KEPT — logged, retried at the next pass.
|
||||
src, hasStamps := g.(DumpStampSource)
|
||||
var dump DataDumpStamp
|
||||
if hasStamps {
|
||||
dump, ok = convertedDumpAt(src.DumpStamps(st.Name), cc, at)
|
||||
}
|
||||
if !hasStamps || !ok {
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] %s: the pre-conversion copy %s is KEPT — a copy proven at %s exists, but no database dump written after %s by PostgreSQL %d is recorded yet", st.Name, cc.Copy, rp.ProvenAt.UTC().Format(time.RFC3339), cc.At, cc.To)
|
||||
}
|
||||
return false
|
||||
}
|
||||
if err := m.copier().Remove(cc.Copy); err != nil {
|
||||
m.logger.Printf("[WARN] [stacks] %s: could not remove the pre-conversion copy %s: %v — kept, tried again later", st.Name, cc.Copy, err)
|
||||
return false
|
||||
}
|
||||
m.logger.Printf("[INFO] [stacks] %s: REMOVED the pre-conversion datadir copy %s (PostgreSQL %d) — the converted app has a backup proven on %d: %s at %s, its dump %s written %s by %v", st.Name, cc.Copy, cc.From, cc.To, updateTierName(rp.Tier), rp.ProvenAt.UTC().Format(time.RFC3339), dump.File, dump.At.UTC().Format(time.RFC3339), dump.Images)
|
||||
return true
|
||||
}
|
||||
|
||||
// ── production boundary ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
type dockerPGConverter struct{ m *Manager }
|
||||
|
||||
Reference in New Issue
Block a user