@@ -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.
3235func 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,75 @@ 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+ return errors .New ("found index" )
131+ },
132+ ); err != nil && ! indexExists {
133+ return false , errors .Wrapf (err , "checking index exists in %s" , subdir )
134+ }
135+ return indexExists , nil
136+ }
137+
138+ // shouldWriteIndex determines if a backup index file should be written for a
139+ // given backup. The rule is:
140+ // 1. An index should only be written on a v25.4+ cluster.
141+ // 2. An incremental backup only writes an index if its parent full has written
142+ // an index file.
143+ //
144+ // This ensures that if a backup chain exists in the index directory, then every
145+ // backup in that chain has an index file, ensuring that the index is usable.
146+ func shouldWriteIndex (
147+ ctx context.Context ,
148+ execCfg * sql.ExecutorConfig ,
149+ store cloud.ExternalStorage ,
150+ details jobspb.BackupDetails ,
151+ ) (bool , error ) {
152+ // This version check can be removed in v26.1 when we no longer need to worry
153+ // about a mixed-version cluster where we have both v25.4+ nodes and pre-v25.4
154+ // nodes.
155+ if ! execCfg .Settings .Version .IsActive (ctx , clusterversion .V25_4 ) {
156+ return false , nil
157+ }
158+
159+ // Full backups can write an index as long as the cluster is on v25.4+.
160+ if details .StartTime .IsEmpty () {
161+ return true , nil
162+ }
163+
164+ return IndexExists (ctx , store , details .Destination .Subdir )
165+ }
166+
98167// getBackupIndexFilePath returns the path to the backup index file representing
99168// a backup that starts and ends at the given timestamps, including
100169// the filename and extension. The path is relative to the collection URI.
101170func getBackupIndexFilePath (subdir string , startTime , endTime hlc.Timestamp ) (string , error ) {
102171 if strings .EqualFold (subdir , backupbase .LatestFileName ) {
103172 return "" , errors .AssertionFailedf ("expected subdir to be resolved and not be 'LATEST'" )
104173 }
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- )
111174 return backuputils .JoinURLPath (
112175 backupbase .BackupIndexDirectoryPath ,
113- flattenedSubdir ,
176+ flattenSubdirForIndex ( subdir ) ,
114177 getBackupIndexFileName (startTime , endTime ),
115178 ), nil
116179}
@@ -130,3 +193,14 @@ func getBackupIndexFileName(startTime, endTime hlc.Timestamp) string {
130193 descEndTs , formattedStartTime , formattedEndTime ,
131194 )
132195}
196+
197+ // flattenSubdirForIndex flattens a full backup subdirectory to be used in the
198+ // index. It assumes subdir is not `LATEST` and has been resolved.
199+ // We flatten the subdir so that when listing from the index, we can list with
200+ // the `index/` prefix and delimit on `/`.
201+ func flattenSubdirForIndex (subdir string ) string {
202+ return strings .ReplaceAll (
203+ strings .TrimPrefix (subdir , "/" ),
204+ "/" , "-" ,
205+ )
206+ }
0 commit comments