Skip to content

Commit 7ff2111

Browse files
committed
Choose most appropriate backup for recovery target
1 parent 39810bf commit 7ff2111

File tree

2 files changed

+166
-0
lines changed

2 files changed

+166
-0
lines changed

internal/restore/restore.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,16 @@ package restore
66
import (
77
"context"
88
"fmt"
9+
"sort"
10+
"time"
911

1012
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
1113
"github.com/cloudnative-pg/cloudnative-pg/pkg/postgres"
1214
restore "github.com/cloudnative-pg/cnpg-i/pkg/restore/job"
15+
"github.com/cloudnative-pg/machinery/pkg/types"
1316
"github.com/dalibo/cnpg-i-pgbackrest/internal/operator"
1417
"github.com/dalibo/cnpg-i-pgbackrest/internal/pgbackrest"
18+
pgbackrestapi "github.com/dalibo/cnpg-i-pgbackrest/internal/pgbackrest/api"
1519
"sigs.k8s.io/controller-runtime/pkg/client"
1620
"sigs.k8s.io/controller-runtime/pkg/log"
1721
)
@@ -111,6 +115,20 @@ func (impl JobHookImpl) Restore(
111115
}
112116
env = append(env, recovEnv...)
113117
pgb := pgbackrest.NewPgBackrest(env)
118+
backupInfo, err := pgb.GetBackupInfo()
119+
if err != nil {
120+
return nil, err
121+
}
122+
choosenBackup, err := selectBestBackup(backupInfo, recovOption)
123+
if err != nil {
124+
return nil, err
125+
}
126+
env = append(
127+
env,
128+
fmt.Sprintf("PGBACKREST_REPO=%d", choosenBackup.Database.RepoKey),
129+
fmt.Sprintf("PGBACKREST_SET=%s", choosenBackup.Label),
130+
)
131+
pgb = pgbackrest.NewPgBackrest(env)
114132
errCh := pgb.Restore(ctx)
115133
if err := <-errCh; err != nil {
116134
return nil, err
@@ -128,3 +146,65 @@ func (impl JobHookImpl) Restore(
128146
Envs: nil,
129147
}, nil
130148
}
149+
150+
func selectBestBackup(
151+
backups []pgbackrestapi.BackupInfo,
152+
opts pgbackrest.RestoreOptions,
153+
) (*pgbackrestapi.BackupInfo, error) {
154+
if len(backups) == 0 {
155+
return nil, fmt.Errorf("no backups available")
156+
}
157+
158+
// First sort backups from newest to oldest, that way we will
159+
// always match the newest backup
160+
sort.Slice(backups, func(i, j int) bool {
161+
return backups[i].Timestamp.Stop > backups[j].Timestamp.Stop
162+
})
163+
164+
switch opts.Type {
165+
// then try to find the most appropriate backup for the type of target
166+
case "name":
167+
for _, b := range backups {
168+
if b.Label == opts.Target {
169+
return &b, nil
170+
}
171+
}
172+
return nil, fmt.Errorf("backup with label %q not found", opts.Target)
173+
174+
case "time":
175+
targetTime, err := time.Parse(time.RFC3339, opts.Target)
176+
if err != nil {
177+
return nil, fmt.Errorf("invalid target time %q: %w", opts.Target, err)
178+
}
179+
targetTS := targetTime.Unix()
180+
181+
for _, b := range backups {
182+
if b.Timestamp.Stop <= targetTS {
183+
return &b, nil
184+
}
185+
}
186+
return nil, fmt.Errorf("no backup found before target time %s", opts.Target)
187+
188+
case "lsn":
189+
targetLSN := types.LSN(opts.Target)
190+
191+
for _, b := range backups {
192+
stopLSN := types.LSN(b.Lsn.Stop)
193+
if stopLSN.Less(targetLSN) || stopLSN == targetLSN {
194+
return &b, nil
195+
}
196+
}
197+
return nil, fmt.Errorf(
198+
"no backup found before target LSN %s",
199+
opts.Target,
200+
)
201+
202+
// decide what to do here
203+
// case "xid":
204+
// return nil, nil
205+
// by default, fallback to the latest backup, may be we should return nothing and
206+
// let pgbackrest do the job by itself
207+
default:
208+
return pgbackrest.LatestBackup(backups), nil
209+
}
210+
}

internal/restore/restore_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ package restore
55

66
import (
77
"testing"
8+
"time"
89

910
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
1011
"github.com/dalibo/cnpg-i-pgbackrest/internal/pgbackrest"
12+
pgbackrestapi "github.com/dalibo/cnpg-i-pgbackrest/internal/pgbackrest/api"
1113
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
1214
)
1315

@@ -105,3 +107,87 @@ func TestRecoveryTargetToRestoreOption(t *testing.T) {
105107
})
106108
}
107109
}
110+
111+
func TestSelectBestBackupForRestore(t *testing.T) {
112+
backups := []pgbackrestapi.BackupInfo{
113+
{
114+
Label: "b1",
115+
Lsn: pgbackrestapi.Lsn{Stop: "0/1000"},
116+
Timestamp: pgbackrestapi.Timestamp{
117+
Stop: 1000,
118+
},
119+
},
120+
{
121+
Label: "b2",
122+
Lsn: pgbackrestapi.Lsn{Stop: "0/2000"},
123+
Timestamp: pgbackrestapi.Timestamp{
124+
Stop: 2000,
125+
},
126+
},
127+
{
128+
Label: "b3",
129+
Lsn: pgbackrestapi.Lsn{Stop: "0/3000"},
130+
Timestamp: pgbackrestapi.Timestamp{
131+
Stop: 3000,
132+
},
133+
},
134+
}
135+
136+
tests := []struct {
137+
name string
138+
opts pgbackrest.RestoreOptions
139+
wantLabel string
140+
expectErr bool
141+
}{
142+
{
143+
name: "select by name",
144+
opts: pgbackrest.RestoreOptions{
145+
Type: "name",
146+
Target: "b2",
147+
},
148+
wantLabel: "b2",
149+
},
150+
{
151+
name: "select by time before b3",
152+
opts: pgbackrest.RestoreOptions{
153+
Type: "time",
154+
Target: time.Unix(2500, 0).Format(time.RFC3339),
155+
},
156+
wantLabel: "b2",
157+
},
158+
{
159+
name: "select by time exactly b3",
160+
opts: pgbackrest.RestoreOptions{
161+
Type: "time",
162+
Target: time.Unix(3000, 0).Format(time.RFC3339),
163+
},
164+
wantLabel: "b3",
165+
},
166+
{
167+
name: "select by LSN <= 0/2500",
168+
opts: pgbackrest.RestoreOptions{
169+
Type: "lsn",
170+
Target: "0/2500",
171+
},
172+
wantLabel: "b2",
173+
},
174+
{
175+
name: "select latest backup by default",
176+
opts: pgbackrest.RestoreOptions{},
177+
wantLabel: "b3",
178+
},
179+
}
180+
181+
for _, tt := range tests {
182+
t.Run(tt.name, func(t *testing.T) {
183+
got, err := selectBestBackup(backups, tt.opts)
184+
if err != nil {
185+
t.Error(err)
186+
}
187+
if got.Label != tt.wantLabel {
188+
t.Errorf("error, got: %v, want: %v", got.Label, tt.wantLabel)
189+
190+
}
191+
})
192+
}
193+
}

0 commit comments

Comments
 (0)