Make snapshot projections self-diagnosing
CI / Test (rest) (pull_request) Successful in 40s
CI / Test (store) (pull_request) Successful in 42s
CI / Lint (pull_request) Successful in 11s
CI / Build (windows/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Test (server-http) (pull_request) Successful in 1m32s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m26s
CI / Test (rest) (pull_request) Successful in 40s
CI / Test (store) (pull_request) Successful in 42s
CI / Lint (pull_request) Successful in 11s
CI / Build (windows/amd64) (pull_request) Successful in 8s
CI / Build (linux/arm64) (pull_request) Successful in 7s
CI / Build (linux/amd64) (pull_request) Successful in 8s
CI / Test (server-http) (pull_request) Successful in 1m32s
e2e / Playwright vs docker-compose (pull_request) Successful in 1m26s
This commit is contained in:
+12
-4
@@ -44,7 +44,7 @@ func (s *Store) LookupHostByAgentToken(ctx context.Context, tokenHash string) (*
|
||||
repo_size_bytes, snapshot_count, open_alert_count,
|
||||
applied_schedule_version, bandwidth_up_kbps, bandwidth_down_kbps,
|
||||
pre_hook_default, post_hook_default,
|
||||
repo_status, repo_status_error, always_on
|
||||
repo_status, repo_status_error, always_on, snapshot_refreshed_at
|
||||
FROM hosts WHERE agent_token_hash = ?`,
|
||||
tokenHash)
|
||||
return scanHost(row)
|
||||
@@ -59,7 +59,7 @@ func (s *Store) GetHost(ctx context.Context, id string) (*Host, error) {
|
||||
repo_size_bytes, snapshot_count, open_alert_count,
|
||||
applied_schedule_version, bandwidth_up_kbps, bandwidth_down_kbps,
|
||||
pre_hook_default, post_hook_default,
|
||||
repo_status, repo_status_error, always_on
|
||||
repo_status, repo_status_error, always_on, snapshot_refreshed_at
|
||||
FROM hosts WHERE id = ?`, id)
|
||||
return scanHost(row)
|
||||
}
|
||||
@@ -227,7 +227,7 @@ func (s *Store) ListHosts(ctx context.Context) ([]Host, error) {
|
||||
repo_size_bytes, snapshot_count, open_alert_count,
|
||||
applied_schedule_version, bandwidth_up_kbps, bandwidth_down_kbps,
|
||||
pre_hook_default, post_hook_default,
|
||||
repo_status, repo_status_error, always_on
|
||||
repo_status, repo_status_error, always_on, snapshot_refreshed_at
|
||||
FROM hosts ORDER BY name`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: list hosts: %w", err)
|
||||
@@ -268,6 +268,7 @@ func scanHostRow(s hostScanner) (*Host, error) {
|
||||
bwUp, bwDown sql.NullInt64
|
||||
preHook, postHook sql.NullString
|
||||
alwaysOn int
|
||||
snapshotRefreshedAt sql.NullString
|
||||
)
|
||||
err := s.Scan(&h.ID, &h.Name, &h.OS, &h.Arch,
|
||||
&h.AgentVersion, &h.ResticVersion, &h.ProtocolVersion,
|
||||
@@ -276,7 +277,7 @@ func scanHostRow(s hostScanner) (*Host, error) {
|
||||
&h.RepoSizeBytes, &h.SnapshotCount, &h.OpenAlertCount,
|
||||
&h.AppliedScheduleVersion, &bwUp, &bwDown,
|
||||
&preHook, &postHook,
|
||||
&h.RepoStatus, &h.RepoStatusError, &alwaysOn)
|
||||
&h.RepoStatus, &h.RepoStatusError, &alwaysOn, &snapshotRefreshedAt)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
@@ -332,6 +333,13 @@ func scanHostRow(s hostScanner) (*Host, error) {
|
||||
h.PostHookDefault = postHook.String
|
||||
}
|
||||
h.AlwaysOn = alwaysOn != 0
|
||||
if snapshotRefreshedAt.Valid {
|
||||
t, err := time.Parse(time.RFC3339Nano, snapshotRefreshedAt.String)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: parse snapshot_refreshed_at: %w", err)
|
||||
}
|
||||
h.SnapshotRefreshedAt = &t
|
||||
}
|
||||
return &h, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,27 @@ type RunningJobActivity struct {
|
||||
LastActivity time.Time
|
||||
}
|
||||
|
||||
// LatestSuccessfulRepoMutation returns the newest completion time for a job
|
||||
// that can change the repository's snapshot projection.
|
||||
func (s *Store) LatestSuccessfulRepoMutation(ctx context.Context, hostID string) (*time.Time, error) {
|
||||
var raw sql.NullString
|
||||
err := s.db.QueryRowContext(ctx, `
|
||||
SELECT MAX(finished_at) FROM jobs
|
||||
WHERE host_id = ? AND status = 'succeeded'
|
||||
AND kind IN ('backup', 'forget', 'prune')`, hostID).Scan(&raw)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: latest successful repo mutation: %w", err)
|
||||
}
|
||||
if !raw.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
t, err := time.Parse(time.RFC3339Nano, raw.String)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: parse latest repo mutation: %w", err)
|
||||
}
|
||||
return &t, nil
|
||||
}
|
||||
|
||||
// ListRunningJobActivity returns every running job with its latest persisted
|
||||
// activity. Jobs without started_at are excluded because they have not actually
|
||||
// entered the running state coherently and cannot be aged safely here.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE hosts ADD COLUMN snapshot_refreshed_at TEXT;
|
||||
@@ -69,8 +69,8 @@ func (s *Store) ReplaceHostSnapshots(ctx context.Context, hostID string, snaps [
|
||||
}
|
||||
|
||||
if _, err := tx.ExecContext(ctx,
|
||||
`UPDATE hosts SET snapshot_count = ? WHERE id = ?`,
|
||||
len(snaps), hostID); err != nil {
|
||||
`UPDATE hosts SET snapshot_count = ?, snapshot_refreshed_at = ? WHERE id = ?`,
|
||||
len(snaps), when.UTC().Format(time.RFC3339Nano), hostID); err != nil {
|
||||
return fmt.Errorf("store: update host snapshot_count: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -138,7 +138,8 @@ func TestReplaceHostSnapshotsEmpty(t *testing.T) {
|
||||
t.Fatalf("replace 1: %v", err)
|
||||
}
|
||||
// Then empty — host has been wiped.
|
||||
if err := s.ReplaceHostSnapshots(ctx, hostID, nil, time.Now().UTC()); err != nil {
|
||||
refreshedAt := time.Now().UTC().Truncate(time.Millisecond)
|
||||
if err := s.ReplaceHostSnapshots(ctx, hostID, nil, refreshedAt); err != nil {
|
||||
t.Fatalf("replace empty: %v", err)
|
||||
}
|
||||
out, err := s.ListSnapshotsByHost(ctx, hostID)
|
||||
@@ -152,4 +153,7 @@ func TestReplaceHostSnapshotsEmpty(t *testing.T) {
|
||||
if h.SnapshotCount != 0 {
|
||||
t.Errorf("snapshot_count should reset to 0, got %d", h.SnapshotCount)
|
||||
}
|
||||
if h.SnapshotRefreshedAt == nil || !h.SnapshotRefreshedAt.Equal(refreshedAt) {
|
||||
t.Errorf("empty projection refresh time = %v, want %s", h.SnapshotRefreshedAt, refreshedAt)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ type Host struct {
|
||||
LastBackupStatus *string
|
||||
RepoSizeBytes int64
|
||||
SnapshotCount int
|
||||
SnapshotRefreshedAt *time.Time
|
||||
OpenAlertCount int
|
||||
AppliedScheduleVersion int64
|
||||
// Host-wide bandwidth caps applied to every restic invocation
|
||||
|
||||
Reference in New Issue
Block a user