Skip to content

Commit c65f9bb

Browse files
committed
Choose most appropriate backup for recovery target
1 parent c3a3637 commit c65f9bb

3 files changed

Lines changed: 159 additions & 0 deletions

File tree

internal/pgbackrest/pgbackrest.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import (
1313
"io"
1414
"os"
1515
"os/exec"
16+
"sort"
1617
"sync"
18+
"time"
1719

1820
pgbackrestapi "github.com/dalibo/cnpg-i-pgbackrest/internal/pgbackrest/api"
1921
"github.com/dalibo/cnpg-i-pgbackrest/internal/utils"
@@ -272,3 +274,69 @@ func (p *PgBackrest) Restore(ctx context.Context) <-chan error {
272274
env = append(env, "PGBACKREST_ARCHIVE_CHECK=n")
273275
return p.runBackgroundTask(ctx, []string{"restore"}, env)
274276
}
277+
278+
func SelectBestBackup(
279+
backups []pgbackrestapi.BackupInfo,
280+
opts RestoreOptions,
281+
) (*pgbackrestapi.BackupInfo, error) {
282+
if len(backups) == 0 {
283+
return nil, fmt.Errorf("no backups available")
284+
}
285+
286+
// First sort backups from newest to oldest, that way we will
287+
// always match the newest backup
288+
sort.Slice(backups, func(i, j int) bool {
289+
return backups[i].Timestamp.Stop > backups[j].Timestamp.Stop
290+
})
291+
292+
switch opts.Type {
293+
case "name":
294+
for _, b := range backups {
295+
if b.Label == opts.Target {
296+
return &b, nil
297+
}
298+
}
299+
return nil, fmt.Errorf("backup with label %q not found", opts.Target)
300+
301+
case "time":
302+
targetTime, err := time.Parse(time.RFC3339, opts.Target)
303+
if err != nil {
304+
return nil, fmt.Errorf("invalid target time %q: %w", opts.Target, err)
305+
}
306+
targetTS := targetTime.Unix()
307+
308+
for _, b := range backups {
309+
if b.Timestamp.Stop <= targetTS {
310+
return &b, nil
311+
}
312+
}
313+
return nil, fmt.Errorf("no backup found before target time %s", opts.Target)
314+
315+
case "lsn":
316+
targetLSN, err := utils.ParseLSN(opts.Target)
317+
if err != nil {
318+
return nil, fmt.Errorf("invalid target LSN %q: %w", opts.Target, err)
319+
}
320+
321+
for _, b := range backups {
322+
stopLSN, err := utils.ParseLSN(b.Lsn.Stop)
323+
if err != nil {
324+
continue
325+
}
326+
if stopLSN <= targetLSN {
327+
return &b, nil
328+
}
329+
}
330+
return nil, fmt.Errorf(
331+
"no backup found before target LSN %s",
332+
opts.Target,
333+
)
334+
335+
// decide what to do here
336+
//case "xid":
337+
// return nil, nil
338+
339+
default:
340+
return LatestBackup(backups), nil
341+
}
342+
}

internal/pgbackrest/pgbackrest_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,3 +420,87 @@ func TestRestoreOptionToEnv(t *testing.T) {
420420
t.Run(tc.desc, f)
421421
}
422422
}
423+
424+
func TestSelectBestBackupForRestore(t *testing.T) {
425+
backups := []pgbackrestapi.BackupInfo{
426+
{
427+
Label: "b1",
428+
Lsn: pgbackrestapi.Lsn{Stop: "0/1000"},
429+
Timestamp: pgbackrestapi.Timestamp{
430+
Stop: 1000,
431+
},
432+
},
433+
{
434+
Label: "b2",
435+
Lsn: pgbackrestapi.Lsn{Stop: "0/2000"},
436+
Timestamp: pgbackrestapi.Timestamp{
437+
Stop: 2000,
438+
},
439+
},
440+
{
441+
Label: "b3",
442+
Lsn: pgbackrestapi.Lsn{Stop: "0/3000"},
443+
Timestamp: pgbackrestapi.Timestamp{
444+
Stop: 3000,
445+
},
446+
},
447+
}
448+
449+
tests := []struct {
450+
name string
451+
opts RestoreOptions
452+
wantLabel string
453+
expectErr bool
454+
}{
455+
{
456+
name: "select by name",
457+
opts: RestoreOptions{
458+
Type: "name",
459+
Target: "b2",
460+
},
461+
wantLabel: "b2",
462+
},
463+
{
464+
name: "select by time before b3",
465+
opts: RestoreOptions{
466+
Type: "time",
467+
Target: time.Unix(2500, 0).Format(time.RFC3339),
468+
},
469+
wantLabel: "b2",
470+
},
471+
{
472+
name: "select by time exactly b3",
473+
opts: RestoreOptions{
474+
Type: "time",
475+
Target: time.Unix(3000, 0).Format(time.RFC3339),
476+
},
477+
wantLabel: "b3",
478+
},
479+
{
480+
name: "select by LSN <= 0/2500",
481+
opts: RestoreOptions{
482+
Type: "lsn",
483+
Target: "0/2500",
484+
},
485+
wantLabel: "b2",
486+
},
487+
{
488+
name: "select latest backup by default",
489+
opts: RestoreOptions{},
490+
wantLabel: "b3",
491+
},
492+
}
493+
494+
for _, tt := range tests {
495+
t.Run(tt.name, func(t *testing.T) {
496+
got, err := SelectBestBackup(backups, tt.opts)
497+
if err != nil {
498+
t.Error(err)
499+
}
500+
if got.Label != tt.wantLabel {
501+
t.Errorf("error, got: %v, want: %v", got.Label, tt.wantLabel)
502+
503+
}
504+
})
505+
}
506+
}

internal/restore/restore.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ func (impl JobHookImpl) Restore(
114114
}
115115
env = append(env, recovEnv...)
116116
pgb := pgbackrest.NewPgBackrest(env)
117+
backupInfo, err := pgb.GetBackupInfo()
118+
choosenBackup, err := pgbackrest.SelectBestBackup(backupInfo, recovOption)
119+
if err != nil {
120+
return nil, err
121+
}
122+
env = append(env, fmt.Sprintf("PGBACKREST_REPO=%d", choosenBackup.Database.RepoKey))
123+
env = append(env, fmt.Sprintf("PGBACKREST_SET=%s", choosenBackup.Label))
117124
errCh := pgb.Restore(ctx)
118125
if err := <-errCh; err != nil {
119126
return nil, err

0 commit comments

Comments
 (0)