Skip to content

Commit e3c4b09

Browse files
craig[bot]kev-cao
andcommitted
Merge #150669
150669: backup: conditionally write backup index for mixed-version r=msbutler a=kev-cao A backup index file is only usable under the following conditions: 1. An index file is written for the full backup of the chain it belongs to. 2. Every incremental in the chain wrote an index. For condition 1 we can ensure this by checking that for incremental backups, the index already exists. In the case of full backups, we can just write the index. However, for 2, we need to ensure that the entire cluster is on v25.4+. Otherwise, it is possible for a full backup to be coordinated by a node that was on 25.4+ and therefore write an index, but for a following incremental to be written by a pre-25.4 node. So we only write indexes if the entire cluster is v25.4+. Epic: [CRDB-47942](https://cockroachlabs.atlassian.net/browse/CRDB-47942) Release note: None Co-authored-by: Kevin Cao <[email protected]>
2 parents 84856d9 + 23c68f3 commit e3c4b09

File tree

5 files changed

+393
-16
lines changed

5 files changed

+393
-16
lines changed

pkg/backup/backup_job.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,7 @@ func backup(
441441

442442
if err := backupdest.WriteBackupIndexMetadata(
443443
ctx,
444+
execCtx.ExecCfg(),
444445
execCtx.User(),
445446
execCtx.ExecCfg().DistSQLSrv.ExternalStorageFromURI,
446447
details,

pkg/backup/backupdest/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,13 @@ go_test(
6060
"//pkg/cloud",
6161
"//pkg/cloud/cloudpb",
6262
"//pkg/cloud/impl:cloudimpl",
63+
"//pkg/clusterversion",
6364
"//pkg/jobs/jobspb",
6465
"//pkg/security/securityassets",
6566
"//pkg/security/securitytest",
6667
"//pkg/security/username",
6768
"//pkg/server",
69+
"//pkg/settings/cluster",
6870
"//pkg/sql",
6971
"//pkg/testutils",
7072
"//pkg/testutils/serverutils",

pkg/backup/backupdest/backup_index.go

Lines changed: 107 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@ import (
99
"bytes"
1010
"context"
1111
"fmt"
12+
"path"
1213
"strings"
1314

1415
"github.com/cockroachdb/cockroach/pkg/backup/backupbase"
1516
"github.com/cockroachdb/cockroach/pkg/backup/backuppb"
1617
"github.com/cockroachdb/cockroach/pkg/backup/backuputils"
1718
"github.com/cockroachdb/cockroach/pkg/cloud"
19+
"github.com/cockroachdb/cockroach/pkg/clusterversion"
1820
"github.com/cockroachdb/cockroach/pkg/jobs/jobspb"
1921
"github.com/cockroachdb/cockroach/pkg/security/username"
22+
"github.com/cockroachdb/cockroach/pkg/sql"
2023
"github.com/cockroachdb/cockroach/pkg/util/hlc"
2124
"github.com/cockroachdb/cockroach/pkg/util/protoutil"
2225
"github.com/cockroachdb/cockroach/pkg/util/tracing"
@@ -31,11 +34,25 @@ import (
3134
// information.
3235
func WriteBackupIndexMetadata(
3336
ctx context.Context,
37+
execCfg *sql.ExecutorConfig,
3438
user username.SQLUsername,
3539
makeExternalStorageFromURI cloud.ExternalStorageFromURIFactory,
3640
details jobspb.BackupDetails,
3741
) error {
38-
ctx, sp := tracing.ChildSpan(ctx, "backupdest.WriteBackupIndexMetadata")
42+
indexStore, err := makeExternalStorageFromURI(
43+
ctx, details.CollectionURI, user,
44+
)
45+
if err != nil {
46+
return errors.Wrapf(err, "creating external storage")
47+
}
48+
49+
if shouldWrite, err := shouldWriteIndex(
50+
ctx, execCfg, indexStore, details,
51+
); !shouldWrite {
52+
return err
53+
}
54+
55+
ctx, sp := tracing.ChildSpan(ctx, "backupinfo.WriteBackupIndexMetadata")
3956
defer sp.Finish()
4057

4158
if details.EndTime.IsEmpty() {
@@ -74,13 +91,6 @@ func WriteBackupIndexMetadata(
7491
return errors.Wrapf(err, "marshal backup index metadata")
7592
}
7693

77-
indexStore, err := makeExternalStorageFromURI(
78-
ctx, details.CollectionURI, user,
79-
)
80-
if err != nil {
81-
return errors.Wrapf(err, "creating external storage")
82-
}
83-
8494
indexFilePath, err := getBackupIndexFilePath(
8595
details.Destination.Subdir,
8696
details.StartTime,
@@ -95,22 +105,78 @@ func WriteBackupIndexMetadata(
95105
)
96106
}
97107

108+
// IndexExists checks if for a given full backup subdirectory there exists a
109+
// corresponding index in the backup collection. This is used to determine when
110+
// we should use the index or the legacy path.
111+
//
112+
// This works under the assumption that we only ever write an index iff:
113+
// 1. For an incremental backup, an index exists for its full backup.
114+
// 2. The backup was taken on a v25.4+ cluster.
115+
//
116+
// The store should be rooted at the default collection URI (the one that
117+
// contains the `index/` directory).
118+
//
119+
// Note: v25.4+ backups will always contain an index file. In other words, we
120+
// can remove these checks in v26.2+.
121+
func IndexExists(ctx context.Context, store cloud.ExternalStorage, subdir string) (bool, error) {
122+
var indexExists bool
123+
indexSubdir := path.Join(backupbase.BackupIndexDirectoryPath, flattenSubdirForIndex(subdir))
124+
if err := store.List(
125+
ctx,
126+
indexSubdir,
127+
"/",
128+
func(file string) error {
129+
indexExists = true
130+
// Because we delimit on `/` and the index subdir does not contain a
131+
// trailing slash, we should only find one file as a result of this list.
132+
// The error is just being returned defensively just in case.
133+
return errors.New("found index")
134+
},
135+
); err != nil && !indexExists {
136+
return false, errors.Wrapf(err, "checking index exists in %s", subdir)
137+
}
138+
return indexExists, nil
139+
}
140+
141+
// shouldWriteIndex determines if a backup index file should be written for a
142+
// given backup. The rule is:
143+
// 1. An index should only be written on a v25.4+ cluster.
144+
// 2. An incremental backup only writes an index if its parent full has written
145+
// an index file.
146+
//
147+
// This ensures that if a backup chain exists in the index directory, then every
148+
// backup in that chain has an index file, ensuring that the index is usable.
149+
func shouldWriteIndex(
150+
ctx context.Context,
151+
execCfg *sql.ExecutorConfig,
152+
store cloud.ExternalStorage,
153+
details jobspb.BackupDetails,
154+
) (bool, error) {
155+
// This version check can be removed in v26.1 when we no longer need to worry
156+
// about a mixed-version cluster where we have both v25.4+ nodes and pre-v25.4
157+
// nodes.
158+
if !execCfg.Settings.Version.IsActive(ctx, clusterversion.V25_4) {
159+
return false, nil
160+
}
161+
162+
// Full backups can write an index as long as the cluster is on v25.4+.
163+
if details.StartTime.IsEmpty() {
164+
return true, nil
165+
}
166+
167+
return IndexExists(ctx, store, details.Destination.Subdir)
168+
}
169+
98170
// getBackupIndexFilePath returns the path to the backup index file representing
99171
// a backup that starts and ends at the given timestamps, including
100172
// the filename and extension. The path is relative to the collection URI.
101173
func getBackupIndexFilePath(subdir string, startTime, endTime hlc.Timestamp) (string, error) {
102174
if strings.EqualFold(subdir, backupbase.LatestFileName) {
103175
return "", errors.AssertionFailedf("expected subdir to be resolved and not be 'LATEST'")
104176
}
105-
// We flatten the subdir so that when listing from the index, we can list with
106-
// the `index/` prefix and delimit on `/`.
107-
flattenedSubdir := strings.ReplaceAll(
108-
strings.TrimPrefix(subdir, "/"),
109-
"/", "-",
110-
)
111177
return backuputils.JoinURLPath(
112178
backupbase.BackupIndexDirectoryPath,
113-
flattenedSubdir,
179+
flattenSubdirForIndex(subdir),
114180
getBackupIndexFileName(startTime, endTime),
115181
), nil
116182
}
@@ -130,3 +196,29 @@ func getBackupIndexFileName(startTime, endTime hlc.Timestamp) string {
130196
descEndTs, formattedStartTime, formattedEndTime,
131197
)
132198
}
199+
200+
// flattenSubdirForIndex flattens a full backup subdirectory to be used in the
201+
// index. Note that this path does not contain a trailing or leading slash.
202+
// It assumes subdir is not `LATEST` and has been resolved.
203+
// We flatten the subdir so that when listing from the index, we can list with
204+
// the `index/` prefix and delimit on `/`. e.g.:
205+
//
206+
// index/
207+
//
208+
// |_ 2025-08-13-120000.00/
209+
// | |_ <index_meta>.pb
210+
// |_ 2025-08-14-120000.00/
211+
// | |_ <index_meta>.pb
212+
// |_ 2025-08-14-120000.00/
213+
// |_ <index_meta>.pb
214+
//
215+
// Listing on `index/` and delimiting on `/` will return the subdirectories
216+
// without listing the files in them.
217+
func flattenSubdirForIndex(subdir string) string {
218+
return strings.ReplaceAll(
219+
// Trimming any trailing and leading slashes guarantees a specific format when
220+
// returning the flattened subdir, so callers can expect a consistent result.
221+
strings.TrimSuffix(strings.TrimPrefix(subdir, "/"), "/"),
222+
"/", "-",
223+
)
224+
}

0 commit comments

Comments
 (0)