fix(dbbackup): 备份新鲜度按最新的 daily 包算,手动或异地包不再掩盖停掉的定时任务
This commit is contained in:
18 files changed
+262
-59
No files matched your search
@@ -16,8 +16,8 @@ import (
|
||||
// something on this install right now".
|
||||
|
||||
// dbBackupView is the wire shape. Last is null until the first backup has been
|
||||
// recorded; Stale is true for a missing record too, so the panel has a single
|
||||
// flag for "nobody could restore today's state".
|
||||
// recorded; Stale is true for a missing record or daily backup too, so the
|
||||
// panel has a single flag for "the daily backups have stopped".
|
||||
type dbBackupView struct {
|
||||
Last *dbbackup.Status `json:"last"`
|
||||
Stale bool `json:"stale"`
|
||||
@@ -43,7 +43,14 @@ func (a *API) handleGetDBBackup(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, r, err)
|
||||
return
|
||||
}
|
||||
// Staleness goes by the daily timer's newest bundle: a manual or
|
||||
// pre-migrate one recorded since would hide a timer that has stopped. A
|
||||
// daily record is its own daily bundle, also when written before daily_at
|
||||
// existed. No daily bundle leaves the zero time, centuries past the limit.
|
||||
if st.Label == dbbackup.LabelDaily {
|
||||
st.DailyAt = st.At
|
||||
}
|
||||
view.Last = &st
|
||||
view.Stale = st.At.IsZero() || a.now().Sub(st.At) > dbbackup.StaleAfter
|
||||
view.Stale = a.now().Sub(st.DailyAt) > dbbackup.StaleAfter
|
||||
writeJSON(w, http.StatusOK, view)
|
||||
}
|
||||
@@ -69,6 +69,41 @@ func TestDBBackupFreshness(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDBBackupDailyDecides: a fresh manual record says nothing about the
|
||||
// timer, so the daily bundle it carries decides; a daily record from before
|
||||
// daily_at existed is its own daily bundle.
|
||||
func TestDBBackupDailyDecides(t *testing.T) {
|
||||
now := time.Date(2026, 9, 24, 12, 0, 0, 0, time.UTC)
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
label string
|
||||
dailyAge time.Duration // 0: no daily_at in the record
|
||||
stale bool
|
||||
wantDaily time.Time
|
||||
}{
|
||||
{"manual over a 30h-old daily", "manual", 30 * time.Hour, true, now.Add(-30 * time.Hour)},
|
||||
{"manual over this morning's daily", "manual", 8 * time.Hour, false, now.Add(-8 * time.Hour)},
|
||||
{"manual with no daily at all", "pre-migrate", 0, true, time.Time{}},
|
||||
{"a daily record written before daily_at", "daily", 0, false, now.Add(-time.Hour)},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
api, repo := seedUpdatesAPI(t)
|
||||
api.Now = func() time.Time { return now }
|
||||
st := dbbackup.Status{At: now.Add(-time.Hour), Name: "felis-db-x-" + tc.label + ".tar", Label: tc.label, Dir: "/var/lib/felis/db-backups"}
|
||||
if tc.dailyAge > 0 {
|
||||
st.DailyAt = now.Add(-tc.dailyAge)
|
||||
}
|
||||
raw, _ := json.Marshal(st)
|
||||
repo.settings[dbbackup.StatusKey] = raw
|
||||
|
||||
code, v := getDBBackup(t, api)
|
||||
if code != http.StatusOK || v.Last == nil || v.Stale != tc.stale || !v.Last.DailyAt.Equal(tc.wantDaily) || !v.Last.At.Equal(st.At) {
|
||||
t.Fatalf("code %d, view %+v, want stale %v daily_at %s", code, v, tc.stale, tc.wantDaily)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDBBackupStoreOutageIsAnError(t *testing.T) {
|
||||
api, repo := seedUpdatesAPI(t)
|
||||
repo.failGetSetting = errors.New("connection reset")
|
||||
|
||||
@@ -17,7 +17,7 @@ import (
|
||||
// response bodies. Each named schema is compared with the Go struct the handler
|
||||
// actually encodes: the property set must equal the struct's JSON field set, and
|
||||
// `required` must list exactly the fields that are always on the wire (no
|
||||
// omitempty). The panel's types are checked against the same schemas at compile
|
||||
// omitempty or omitzero). The panel's types are checked against the same schemas at compile
|
||||
// time (panel/src/lib/types.parity.ts), so a field added here without the docs
|
||||
// fails in Go, and one added to the docs without the panel fails in tsc.
|
||||
func TestOpenAPISchemasMatchWireStructs(t *testing.T) {
|
||||
@@ -125,7 +125,10 @@ func wireFields(t reflect.Type) map[string]wireField {
|
||||
if name == "" {
|
||||
name = f.Name
|
||||
}
|
||||
out[name] = wireField{omitempty: strings.Contains(","+opts+",", ",omitempty,"), typ: f.Type}
|
||||
// omitzero leaves a field out too, and is the one that does for a
|
||||
// struct such as time.Time.
|
||||
opts = "," + opts + ","
|
||||
out[name] = wireField{omitempty: strings.Contains(opts, ",omitempty,") || strings.Contains(opts, ",omitzero,"), typ: f.Type}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user