-
Notifications
You must be signed in to change notification settings - Fork 3.8k
/
mixed_version_backup.go
2135 lines (1877 loc) · 74 KB
/
mixed_version_backup.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// Copyright 2023 The Cockroach Authors.
//
// Use of this software is governed by the Business Source License
// included in the file licenses/BSL.txt.
//
// As of the Change Date specified in that file, in accordance with
// the Business Source License, use of this software will be governed
// by the Apache License, Version 2.0, included in the file
// licenses/APL.txt.
package tests
import (
"context"
gosql "database/sql"
"encoding/json"
"fmt"
"math/rand"
"os"
"path/filepath"
"reflect"
"regexp"
"sort"
"strings"
"sync/atomic"
"time"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/cluster"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/option"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/registry"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/roachtestutil"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/roachtestutil/clusterupgrade"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/roachtestutil/mixedversion"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/spec"
"github.com/cockroachdb/cockroach/pkg/cmd/roachtest/test"
"github.com/cockroachdb/cockroach/pkg/jobs"
"github.com/cockroachdb/cockroach/pkg/jobs/jobspb"
"github.com/cockroachdb/cockroach/pkg/roachprod/logger"
"github.com/cockroachdb/cockroach/pkg/testutils"
"github.com/cockroachdb/cockroach/pkg/testutils/jobutils"
"github.com/cockroachdb/cockroach/pkg/util/hlc"
"github.com/cockroachdb/cockroach/pkg/util/protoutil"
"github.com/cockroachdb/cockroach/pkg/util/randutil"
"github.com/cockroachdb/cockroach/pkg/util/retry"
"github.com/cockroachdb/cockroach/pkg/util/timeutil"
"github.com/cockroachdb/cockroach/pkg/util/version"
"golang.org/x/sync/errgroup"
)
const (
// nonceLen is the length of the randomly generated string appended
// to the backup name when creating a backup in GCS for this test.
nonceLen = 4
// systemTableSentinel is a placeholder value for system table
// columns that cannot be validated after restore, for various
// reasons. See `handleSpecialCases` for more details.
systemTableSentinel = "{SENTINEL}"
// probability that we will attempt to restore a backup in
// mixed-version state.
mixedVersionRestoreProbability = 0.5
// suffix added to the names of backups taken while the cluster is
// upgrading.
finalizingSuffix = "_finalizing"
// probabilities that the test will attempt to pause a backup job.
neverPause = 0
alwaysPause = 1
// the test will not pause a backup job more than `maxPauses` times.
maxPauses = 3
)
var (
invalidVersionRE = regexp.MustCompile(`[^a-zA-Z0-9\.]`)
invalidDBNameRE = regexp.MustCompile(`[\-\.:/]`)
// retry options while waiting for a backup to complete
backupCompletionRetryOptions = retry.Options{
InitialBackoff: 10 * time.Second,
MaxBackoff: 1 * time.Minute,
Multiplier: 1.5,
MaxRetries: 80,
}
v231 = func() *version.Version {
v, err := version.Parse("v23.1.0")
if err != nil {
panic(fmt.Sprintf("failure parsing version: %v", err))
}
return v
}()
// systemTablesInFullClusterBackup includes all system tables that
// are included as part of a full cluster backup. It should include
// every table that opts-in to cluster backup (see `system_schema.go`).
// It should be updated as system tables are added or removed from
// cluster backups.
//
// Note that we don't verify the `system.zones` table as there's no
// general mechanism to verify its correctness due to #100852. We
// may change that if the issue is fixed.
systemTablesInFullClusterBackup = []string{
"users", "settings", "locations", "role_members", "role_options", "ui",
"comments", "scheduled_jobs", "database_role_settings", "tenant_settings",
"privileges", "external_connections",
}
// showSystemQueries maps system table names to `SHOW` statements
// that reflect their contents in a more human readable format. When
// the contents of a system table after restore does not match the
// values present when the backup was taken, we show the output of
// the "SHOW" startements to make debugging failures easier.
showSystemQueries = map[string]string{
"privileges": "SHOW GRANTS",
"role_members": "SHOW ROLES",
"settings": "SHOW CLUSTER SETTINGS",
"users": "SHOW USERS",
"zones": "SHOW ZONE CONFIGURATIONS",
}
// systemSettingValues is a mapping from system setting names to
// possible values they can assume in this test. The system settings
// listed here are chosen based on availability of documentation
// (see below), meaning a customer could have reasonably set them;
// and relationship to backup/restore. The list of possible values
// are generally 50-100% smaller or larger than the documented
// default.
//
// Documentation:
// https://www.cockroachlabs.com/docs/stable/cluster-settings.html
systemSettingValues = map[string][]string{
"bulkio.backup.file_size": {"8MiB", "32MiB", "512MiB", "750MiB"},
"bulkio.backup.read_timeout": {"2m0s", "10m0s"},
"bulkio.backup.read_with_priority_after": {"20s", "5m0s"},
"bulkio.stream_ingestion.minimum_flush_interval": {"1s", "10s", "30s"},
"kv.bulk_io_write.max_rate": {"250MiB", "500MiB", "2TiB"},
"kv.bulk_sst.max_allowed_overage": {"16MiB", "256MiB"},
"kv.bulk_sst.target_size": {"4MiB", "64MiB", "128MiB"},
}
systemSettingNames = func() []string {
names := make([]string, 0, len(systemSettingValues))
for name := range systemSettingValues {
names = append(names, name)
}
// Sort the settings names so that picking a random setting is
// deterministic, given the same random seed.
sort.Strings(names)
return names
}()
bankPossibleRows = []int{
100, // creates keys with long revision history
1_000, // small backup
10_000, // larger backups (a few GiB when using 128 KiB payloads)
}
bankPossiblePayloadBytes = []int{
0, // workload default
9, // 1 random byte (`initial-` + 1)
500, // 5x default at the time of writing
16 << 10, // 16 KiB
128 << 10, // 128 KiB
}
)
// sanitizeVersionForBackup takes the string representation of a
// version and removes any characters that would not be allowed in a
// backup destination.
func sanitizeVersionForBackup(v string) string {
return invalidVersionRE.ReplaceAllString(clusterupgrade.VersionMsg(v), "")
}
// hasInternalSystemJobs returns true if the cluster is expected to
// have the `crdb_internal.system_jobs` vtable in the mixed-version
// context passed. If so, it should be used instead of `system.jobs`
// when querying job status.
func hasInternalSystemJobs(tc *mixedversion.Context) bool {
lowestVersion := tc.FromVersion // upgrades
if tc.FromVersion == clusterupgrade.MainVersion {
lowestVersion = tc.ToVersion // downgrades
}
// Add 'v' prefix expected by `version` package.
lowestVersion = "v" + lowestVersion
sv, err := version.Parse(lowestVersion)
if err != nil {
panic(fmt.Errorf("internal error: test context version (%s) expected to be parseable: %w", lowestVersion, err))
}
return sv.AtLeast(v231)
}
func aostFor(timestamp string) string {
var stmt string
if timestamp != "" {
stmt = fmt.Sprintf(" AS OF SYSTEM TIME '%s'", timestamp)
}
return stmt
}
// quoteColumnsForSelect quotes every column passed as parameter and
// returns a string that can be passed to a `SELECT` statement. Since
// we are dynamically loading the columns for a number of system
// tables, we need to quote the column names to avoid SQL syntax
// errors.
func quoteColumnsForSelect(columns []string) string {
quoted := make([]string, 0, len(columns))
for _, c := range columns {
quoted = append(quoted, fmt.Sprintf("%q", c))
}
return strings.Join(quoted, ", ")
}
type (
// backupOption is an option passed to the `BACKUP` command (i.e.,
// `WITH ...` portion).
backupOption interface {
fmt.Stringer
}
// revisionHistory wraps the `revision_history` backup option.
revisionHistory struct{}
// encryptionPassphrase is the `encryption_passphrase` backup
// option. If passed when a backup is created, the same passphrase
// needs to be passed for incremental backups and for restoring the
// backup as well.
encryptionPassphrase struct {
passphrase string
}
// backupType is the interface to be implemented by each backup type
// (table, database, cluster).
backupType interface {
// Desc returns a string describing the backup type, and is used
// when creating the name for a backup collection.
Desc() string
// BackupCommand returns the `BACKUP {target}` part of the backup
// command used by the test to create the backup, up until the
// `INTO` keyword.
BackupCommand() string
// RestoreCommand returns the `RESTORE {target}` part of the
// restore command used by the test to restore a backup, up until
// the `FROM` keyword. This function can also return any options
// to be used when restoring. The string parameter is the name of
// a unique database created by the test harness that restores
// should use to restore their data, if applicable.
RestoreCommand(string) (string, []string)
// TargetTables returns a list of tables that are part of the
// backup collection. These tables will be verified when the
// backup is restored, and they should have the same contents as
// they did when the backup was taken.
TargetTables() []string
}
// tableBackup -- BACKUP TABLE ...
tableBackup struct {
db string
target string
}
// databaseBackup -- BACKUP DATABASE ...
databaseBackup struct {
db string
tables []string
}
// clusterBackup -- BACKUP ...
clusterBackup struct {
dbBackups []*databaseBackup
systemTables []string
}
// tableContents is an interface to be implemented by different
// methods of storing and comparing the contents of a table. A lot
// of backup-related tests use the `EXPERIMENTAL_FINGERPRINTS`
// functionality to cheaply compare the contents of a table before a
// backup and after restore; however, the semantics of system tables
// does not allow for that comparison as changes in system table
// contents after an upgrade are expected.
tableContents interface {
// Load computes the contents of a table with the given name
// (passed as argument) and stores its representation internally.
// If the contents of the table are being loaded from a restore,
// the corresponding `tableContents` are passed as parameter as
// well (`nil` otherwise).
Load(context.Context, *logger.Logger, string, tableContents) error
// ValidateRestore validates that a restored table with contents
// passed as argument is valid according to the table contents
// previously `Load`ed.
ValidateRestore(context.Context, *logger.Logger, tableContents) error
}
// fingerprintContents implements the `tableContents` interface
// using the `EXPERIMENTAL_FINGERPRINTS` functionality. This should
// be the implementation for all user tables.
fingerprintContents struct {
db *gosql.DB
table string
fingerprints string
}
// showSystemResults stores the results of a SHOW statement when the
// contents of a system table are inspected. Both the specific query
// used and the output are stored.
showSystemResults struct {
query string
output string
}
// systemTableRow encapsulates the contents of a row in a system
// table and provides a more declarative API for the test to
// override certain columns with sentinel values when we cannot
// expect the restored value to match the value at the time the
// backup was taken.
systemTableRow struct {
table string
values []interface{}
columns []string
matches bool
err error
}
// systemTableContents implements the `tableContents` interface for
// system tables. Since the contents and the schema of system tables
// may change as the database upgrades, we can't use fingerprints.
// Instead, this struct validates that any data that existed in a
// system table at the time of the backup should continue to exist
// when the backup is restored.
systemTableContents struct {
cluster cluster.Cluster
roachNode int
db *gosql.DB
table string
columns []string
rows map[string]struct{}
showResults *showSystemResults
}
// backupCollection wraps a backup collection (which may or may not
// contain incremental backups). The associated fingerprint is the
// expected fingerprint when the corresponding table is restored.
backupCollection struct {
btype backupType
name string
options []backupOption
nonce string
// tables is the list of tables that we are going to verify once
// this backup is restored. There should be a 1:1 mapping between
// the table names in `tables` and the `tableContents` implementations
// in the `contents` field.
tables []string
contents []tableContents
}
fullBackup struct {
label string
}
incrementalBackup struct {
collection backupCollection
}
// labeledNodes allows us to label a set of nodes with the version
// they are running, to allow for human-readable backup names
labeledNodes struct {
Nodes option.NodeListOption
Version string
}
// backupSpec indicates where backups are supposed to be planned
// (`BACKUP` statement sent to); and where they are supposed to be
// executed (where the backup job will be picked up).
backupSpec struct {
PauseProbability float64
Plan labeledNodes
Execute labeledNodes
}
)
// tableNamesWithDB returns a list of qualified table names where the
// database name is `db`. This can be useful in situations where we
// backup a "bank.bank" table, for example, and then restore it into a
// "some_db" database. The table name in the restored database would
// be "some_db.bank".
//
// Example:
//
// tableNamesWithDB("restored_db", []string{"bank.bank", "stock"})
// => []string{"restored_db.bank", "restored_db.stock"}
func tableNamesWithDB(db string, tables []string) []string {
names := make([]string, 0, len(tables))
for _, t := range tables {
parts := strings.Split(t, ".")
var tableName string
if len(parts) == 1 {
tableName = parts[0]
} else {
tableName = parts[1]
}
names = append(names, fmt.Sprintf("%s.%s", db, tableName))
}
return names
}
func (fb fullBackup) String() string { return "full" }
func (ib incrementalBackup) String() string { return "incremental" }
func (rh revisionHistory) String() string {
return "revision_history"
}
func randIntBetween(rng *rand.Rand, min, max int) int {
return rng.Intn(max-min) + min
}
func randString(rng *rand.Rand, strLen int) string {
return randutil.RandString(rng, strLen, randutil.PrintableKeyAlphabet)
}
func randWaitDuration(rng *rand.Rand) time.Duration {
durations := []int{1, 10, 60, 5 * 60}
return time.Duration(durations[rng.Intn(len(durations))]) * time.Second
}
func newEncryptionPassphrase(rng *rand.Rand) encryptionPassphrase {
return encryptionPassphrase{randString(rng, randIntBetween(rng, 32, 64))}
}
func (ep encryptionPassphrase) String() string {
return fmt.Sprintf("encryption_passphrase = '%s'", ep.passphrase)
}
// newBackupOptions returns a list of backup options to be used when
// creating a new backup. Each backup option has a 50% chance of being
// included.
func newBackupOptions(rng *rand.Rand) []backupOption {
possibleOpts := []backupOption{
revisionHistory{},
newEncryptionPassphrase(rng),
}
var options []backupOption
for _, opt := range possibleOpts {
if rng.Float64() < 0.5 {
options = append(options, opt)
}
}
return options
}
// newCommentTarget returns either a database or a table to be used as
// a target for a `COMMENT ON` statement. Returns the object being
// commented (either 'database' or 'table'), and the name of the
// object itself.
func newCommentTarget(rng *rand.Rand, dbs []string, tables [][]string) (string, string) {
const dbCommentProbability = 0.4
targetDBIdx := rng.Intn(len(dbs))
targetDB := dbs[targetDBIdx]
if rng.Float64() < dbCommentProbability {
return "database", targetDB
}
dbTables := tables[targetDBIdx]
targetTable := dbTables[rng.Intn(len(dbTables))]
return "table", fmt.Sprintf("%s.%s", targetDB, targetTable)
}
func newTableBackup(rng *rand.Rand, dbs []string, tables [][]string) *tableBackup {
var targetDBIdx int
var targetDB string
// Avoid creating table backups for the tpcc database, as they often
// have foreign keys to other tables, making restoring them
// difficult. We could pass the `skip_missing_foreign_keys` option,
// but that would be a less interesting test.
for targetDB == "" || targetDB == "tpcc" {
targetDBIdx = rng.Intn(len(dbs))
targetDB = dbs[targetDBIdx]
}
dbTables := tables[targetDBIdx]
targetTable := dbTables[rng.Intn(len(dbTables))]
return &tableBackup{dbs[targetDBIdx], targetTable}
}
func (tb *tableBackup) Desc() string {
return fmt.Sprintf("table %s.%s", tb.db, tb.target)
}
func (tb *tableBackup) BackupCommand() string {
return fmt.Sprintf("BACKUP TABLE %s", tb.TargetTables()[0])
}
func (tb *tableBackup) RestoreCommand(restoreDB string) (string, []string) {
options := []string{fmt.Sprintf("into_db = '%s'", restoreDB)}
return fmt.Sprintf("RESTORE TABLE %s", tb.TargetTables()[0]), options
}
func (tb *tableBackup) TargetTables() []string {
return []string{fmt.Sprintf("%s.%s", tb.db, tb.target)}
}
func newDatabaseBackup(rng *rand.Rand, dbs []string, tables [][]string) *databaseBackup {
targetDBIdx := rng.Intn(len(dbs))
return &databaseBackup{dbs[targetDBIdx], tables[targetDBIdx]}
}
func (dbb *databaseBackup) Desc() string {
return fmt.Sprintf("database %s", dbb.db)
}
func (dbb *databaseBackup) BackupCommand() string {
return fmt.Sprintf("BACKUP DATABASE %s", dbb.db)
}
func (dbb *databaseBackup) RestoreCommand(restoreDB string) (string, []string) {
options := []string{fmt.Sprintf("new_db_name = '%s'", restoreDB)}
return fmt.Sprintf("RESTORE DATABASE %s", dbb.db), options
}
func (dbb *databaseBackup) TargetTables() []string {
return tableNamesWithDB(dbb.db, dbb.tables)
}
func newClusterBackup(rng *rand.Rand, dbs []string, tables [][]string) *clusterBackup {
dbBackups := make([]*databaseBackup, 0, len(dbs))
for j, db := range dbs {
dbBackups = append(dbBackups, newDatabaseBackup(rng, []string{db}, [][]string{tables[j]}))
}
return &clusterBackup{
dbBackups: dbBackups,
systemTables: systemTablesInFullClusterBackup,
}
}
func (cb *clusterBackup) Desc() string { return "cluster" }
func (cb *clusterBackup) BackupCommand() string { return "BACKUP" }
func (cb *clusterBackup) RestoreCommand(_ string) (string, []string) { return "RESTORE", nil }
func (cb *clusterBackup) TargetTables() []string {
var dbTargetTables []string
for _, dbb := range cb.dbBackups {
dbTargetTables = append(dbTargetTables, dbb.TargetTables()...)
}
return append(dbTargetTables, tableNamesWithDB("system", cb.systemTables)...)
}
func newFingerprintContents(db *gosql.DB, table string) *fingerprintContents {
return &fingerprintContents{db: db, table: table}
}
// Load computes the fingerprints for the underlying table and stores
// the contents in the `fingeprints` field.
func (fc *fingerprintContents) Load(
ctx context.Context, l *logger.Logger, timestamp string, _ tableContents,
) error {
l.Printf("computing fingerprints for table %s", fc.table)
query := fmt.Sprintf(
"SELECT index_name, fingerprint FROM [SHOW EXPERIMENTAL_FINGERPRINTS FROM TABLE %s]%s ORDER BY index_name",
fc.table, aostFor(timestamp),
)
rows, err := fc.db.QueryContext(ctx, query)
if err != nil {
return fmt.Errorf("error when running query [%s]: %w", query, err)
}
defer rows.Close()
var fprints []string
for rows.Next() {
var indexName, fprint string
if err := rows.Scan(&indexName, &fprint); err != nil {
return fmt.Errorf("error computing fingerprint for table %s, index %s: %w", fc.table, indexName, err)
}
fprints = append(fprints, fmt.Sprintf("%s:%s", indexName, fprint /* actualFingerprint */))
}
if err := rows.Err(); err != nil {
return fmt.Errorf("error iterating over fingerprint rows for table %s: %w", fc.table, err)
}
fc.fingerprints = strings.Join(fprints, "\n")
return nil
}
// ValidateRestore compares the fingerprints associated with the given
// restored contents (which are assumed to be
// `fingerprintContents`). Returns an error if the fingerprints don't
// match.
func (fc *fingerprintContents) ValidateRestore(
ctx context.Context, l *logger.Logger, contents tableContents,
) error {
restoredContents := contents.(*fingerprintContents)
if fc.fingerprints != restoredContents.fingerprints {
l.Printf(
"mismatched fingerprints for table %s\n--- Expected:\n%s\n--- Actual:\n%s",
fc.table, fc.fingerprints, restoredContents.fingerprints,
)
return fmt.Errorf("mismatched fingerprints for table %s", fc.table)
}
return nil
}
func newSystemTableRow(table string, values []interface{}, columns []string) *systemTableRow {
return &systemTableRow{table: table, values: values, columns: columns, matches: true}
}
// skip determines whether we should continue applying filters or
// replacing columns value in an instance of systemTableRow. `matches`
// will be false if the caller used `Matches` and the column value did
// not match. `err` includes any error found while performing changes
// in this system table row, in which case the error should be
// returned to the caller.
func (sr *systemTableRow) skip() bool {
return !sr.matches || sr.err != nil
}
// processColumn takes a column name and a callback function; the
// callback will be passed the value associated with that column, if
// found, and its return value overwrites the value for that
// column. The callback is not called if the column name is invalid.
func (sr *systemTableRow) processColumn(column string, fn func(interface{}) interface{}) {
if sr.skip() {
return
}
colIdx := sort.SearchStrings(sr.columns, column)
hasCol := colIdx < len(sr.columns) && sr.columns[colIdx] == column
if !hasCol {
sr.err = fmt.Errorf("could not find column %s on %s", column, sr.table)
return
}
sr.values[colIdx] = fn(sr.values[colIdx])
}
// Matches allows the caller to only apply certain changes if the
// value of a certain column matches a given value.
func (sr *systemTableRow) Matches(column string, value interface{}) *systemTableRow {
sr.processColumn(column, func(actualValue interface{}) interface{} {
sr.matches = reflect.DeepEqual(actualValue, value)
return actualValue
})
return sr
}
// WithSentinel replaces the contents of the given columns with a
// fixed sentinel value.
func (sr *systemTableRow) WithSentinel(columns ...string) *systemTableRow {
for _, column := range columns {
sr.processColumn(column, func(value interface{}) interface{} {
return systemTableSentinel
})
}
return sr
}
// Values must be called when all column manipulations have been
// made. It returns the final set of values to be used for the system
// table row, and any error found along the way.
func (sr *systemTableRow) Values() ([]interface{}, error) {
return sr.values, sr.err
}
func newSystemTableContents(
ctx context.Context, c cluster.Cluster, node int, db *gosql.DB, name, timestamp string,
) (*systemTableContents, error) {
// Dynamically load column names for the corresponding system
// table. We use an AOST clause as this may be happening while
// upgrade migrations are in-flight (which may change the schema of
// system tables).
query := fmt.Sprintf(
"SELECT column_name FROM [SHOW COLUMNS FROM %s]%s ORDER BY column_name",
name, aostFor(timestamp),
)
rows, err := db.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("error querying column names for %s: %w", name, err)
}
var columnNames []string
for rows.Next() {
var colName string
if err := rows.Scan(&colName); err != nil {
return nil, fmt.Errorf("error scanning column_name for table %s: %w", name, err)
}
columnNames = append(columnNames, colName)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("error iterating over columns of %s: %w", name, err)
}
return &systemTableContents{
cluster: c, db: db, roachNode: node, table: name, columns: columnNames,
}, nil
}
// RawFormat displays the contents of a system table as serialized
// during `Load`. The contents of the system table may not be
// immediately understandable as they might include protobuf payloads
// and other binary-encoded data.
func (sc *systemTableContents) RawFormat() string {
rows := make([]string, 0, len(sc.rows))
for r := range sc.rows {
rows = append(rows, r)
}
return strings.Join(rows, "\n")
}
// ShowSystemOutput displays the contents of the the `SHOW` statement
// for the underlying system table, if available.
func (sc *systemTableContents) ShowSystemOutput(label string) string {
if sc.showResults == nil {
return ""
}
return fmt.Sprintf("\n--- %q %s:\n%s", sc.showResults.query, label, sc.showResults.output)
}
// settingsHandler replaces the contents of every column in the
// system.settings table for the `version` setting. This setting is
// expected to contain the current cluster version, so it should not
// match the value when the backup was taken.
func (sc *systemTableContents) settingsHandler(
values []interface{}, columns []string,
) ([]interface{}, error) {
return newSystemTableRow(sc.table, values, columns).
// `name` column equals 'version'
Matches("name", "version").
// use the sentinel value for every column in the settings table
WithSentinel(columns...).
Values()
}
// scheduleJobsHandler replaces the contents of some columns in the
// `scheduled_jobs` system table since we cannot ensure that they will
// match their values in the backup. Specifically, there's a race in
// the case when a scheduled jobs's next_run is in the past. In this
// scenario, the jobs subssytem will schedule the job for execution as
// soon as the restore is finished, and by the time the test attempts
// to compare the restored contents of scheduled_jobs with the
// original contents, they will have already diverged.
//
// Note that this same race was also identified in unit tests (see #100094).
func (sc *systemTableContents) scheduledJobsHandler(
values []interface{}, columns []string,
) ([]interface{}, error) {
return newSystemTableRow(sc.table, values, columns).
WithSentinel("next_run", "schedule_details", "schedule_state").
Values()
}
func (sc *systemTableContents) commentsHandler(
values []interface{}, columns []string,
) ([]interface{}, error) {
return newSystemTableRow(sc.table, values, columns).
WithSentinel("object_id"). // object_id is rekeyed
Values()
}
// handleSpecialCases exists because there are still cases where we
// can't assume that the contents of a system table are the same after
// a RESTORE. Columns that cannot be expected to be the same are
// replaced with a sentinel value in this function.
func (sc *systemTableContents) handleSpecialCases(
l *logger.Logger, row []interface{}, columns []string,
) ([]interface{}, error) {
switch sc.table {
case "system.settings":
return sc.settingsHandler(row, columns)
case "system.scheduled_jobs":
return sc.scheduledJobsHandler(row, columns)
case "system.comments":
return sc.commentsHandler(row, columns)
default:
return row, nil
}
}
// loadShowResults loads the `showResults` field of the
// `systemTableContents` struct if the system table being analyzed has
// a matching `SHOW` statement (see `showSystemQueries` mapping). We
// run `cockroach sql` directly here to leverage the existing
// formatting logic in that command. This is only done to facilitate
// debugging in the event of failures.
func (sc *systemTableContents) loadShowResults(
ctx context.Context, l *logger.Logger, timestamp string,
) error {
systemName := strings.TrimPrefix(sc.table, "system.")
showStmt, ok := showSystemQueries[systemName]
if !ok {
return nil
}
query := fmt.Sprintf("SELECT * FROM [%s]%s", showStmt, aostFor(timestamp))
showCmd := roachtestutil.NewCommand("%s sql", mixedversion.CurrentCockroachPath).
Option("insecure").
Flag("e", fmt.Sprintf("%q", query)).
String()
node := sc.cluster.Node(sc.roachNode)
result, err := sc.cluster.RunWithDetailsSingleNode(ctx, l, node, showCmd)
if err != nil {
return fmt.Errorf("error running command (%s): %w", showCmd, err)
}
sc.showResults = &showSystemResults{query: query, output: result.Stdout}
return nil
}
// Load loads the contents of the underlying system table in memory.
// The contents of every row in the system table are marshaled using
// `json_agg` in the database. This function then converts that to a
// JSON array and uses that serialized representation in a set of rows
// that are kept in memory.
//
// Some assumptions this function makes:
// * all the contents of a system table can be marshaled to JSON;
// * the entire contents of system tables can be kept in memory.
//
// For the purposes of this test, both of these assumptions should
// hold without problems at the time of writing (~v23.1). We may need
// to revisit them if that ever changes.
func (sc *systemTableContents) Load(
ctx context.Context, l *logger.Logger, timestamp string, previous tableContents,
) error {
var loadColumns []string
// If we are loading the contents of a system table after a restore,
// we should only be querying the columns that were loaded when the
// corresponding backup happened. We can't verify the correctness of
// data added by migrations.
if previous == nil {
loadColumns = sc.columns
} else {
previousSystemTableContents := previous.(*systemTableContents)
loadColumns = previousSystemTableContents.columns
}
l.Printf("loading data in table %s", sc.table)
inner := fmt.Sprintf("SELECT %s FROM %s", quoteColumnsForSelect(loadColumns), sc.table)
query := fmt.Sprintf(
"SELECT coalesce(json_agg(s), '[]') AS encoded_data FROM (%s) s%s",
inner, aostFor(timestamp),
)
var data []byte
if err := sc.db.QueryRowContext(ctx, query).Scan(&data); err != nil {
return fmt.Errorf("error when running query [%s]: %w", query, err)
}
// opaqueRows should contain a list of JSON-encoded rows:
// [{"col1": "val11", "col2": "val12"}, {"col1": "val21", "col2": "val22"}, ...]
var opaqueRows []map[string]interface{}
if err := json.Unmarshal(data, &opaqueRows); err != nil {
return fmt.Errorf("error unmarshaling data from table %s: %w", sc.table, err)
}
// rowSet should be a set of rows where each row is encoded as a
// JSON array that matches the sorted list of columns in
// `sc.columns`:
// {["val11", "val12"], ["val21", "val22"], ...}
rowSet := make(map[string]struct{})
for _, r := range opaqueRows {
opaqueRow := make([]interface{}, 0, len(loadColumns))
for _, c := range loadColumns {
opaqueRow = append(opaqueRow, r[c])
}
processedRow, err := sc.handleSpecialCases(l, opaqueRow, loadColumns)
if err != nil {
return fmt.Errorf("error processing row %v: %w", opaqueRow, err)
}
encodedRow, err := json.Marshal(processedRow)
if err != nil {
return fmt.Errorf("error marshaling processed row for table %s: %w", sc.table, err)
}
rowSet[string(encodedRow)] = struct{}{}
}
sc.rows = rowSet
if err := sc.loadShowResults(ctx, l, timestamp); err != nil {
return err
}
l.Printf("loaded %d rows from %s", len(sc.rows), sc.table)
return nil
}
// ValidateRestore validates that every row that existed in a system
// table when the backup was taken continues to exist when the backup
// is restored.
func (sc *systemTableContents) ValidateRestore(
ctx context.Context, l *logger.Logger, contents tableContents,
) error {
restoredContents := contents.(*systemTableContents)
for originalRow := range sc.rows {
_, exists := restoredContents.rows[originalRow]
if !exists {
// Log the missing row and restored table contents here and
// avoid including it in the error itself as the error message
// from a test is displayed multiple times in a roachtest
// failure, and having a long, multi-line error message adds a
// lot of noise to the logs.
l.Printf(
"--- Missing row in table %s:\n%s\n--- Original rows:\n%s\n--- Restored contents:\n%s%s%s",
sc.table, originalRow, sc.RawFormat(), restoredContents.RawFormat(),
sc.ShowSystemOutput("when backup was taken"), restoredContents.ShowSystemOutput("after restore"),
)
return fmt.Errorf("restored system table %s is missing a row: %s", sc.table, originalRow)
}
}
return nil
}
func newBackupCollection(name string, btype backupType, options []backupOption) backupCollection {
// Use a different seed for generating the collection's nonce to
// allow for multiple concurrent runs of this test using the same
// COCKROACH_RANDOM_SEED, making it easier to reproduce failures
// that are more likely to occur with certain test plans.
nonceRng := rand.New(rand.NewSource(timeutil.Now().UnixNano()))
return backupCollection{
btype: btype,
name: name,
tables: btype.TargetTables(),
options: options,
nonce: randString(nonceRng, nonceLen),
}
}
func (bc *backupCollection) uri() string {
// Append the `nonce` to the backup name since we are now sharing a
// global namespace represented by the cockroachdb-backup-testing
// bucket. The nonce allows multiple people (or TeamCity builds) to
// be running this test without interfering with one another.
return fmt.Sprintf("gs://cockroachdb-backup-testing/mixed-version/%s_%s?AUTH=implicit", bc.name, bc.nonce)
}
func (bc *backupCollection) encryptionOption() *encryptionPassphrase {
for _, option := range bc.options {
if ep, ok := option.(encryptionPassphrase); ok {
return &ep
}
}
return nil
}
// backupCollectionDesc builds a string that describes how a backup
// collection comprised of a full backup and a follow-up incremental
// backup was generated (in terms of which versions planned vs
// executed the backup). Used to generate descriptive backup names.
func backupCollectionDesc(fullSpec, incSpec backupSpec) string {
specMsg := func(label string, s backupSpec) string {
if s.Plan.Version == s.Execute.Version {
return fmt.Sprintf("%s planned and executed on %s", label, s.Plan.Version)
}
return fmt.Sprintf("%s planned on %s executed on %s", label, s.Plan.Version, s.Execute.Version)
}
if reflect.DeepEqual(fullSpec, incSpec) {
return specMsg("all", fullSpec)
}
return fmt.Sprintf("%s %s", specMsg("full", fullSpec), specMsg("incremental", incSpec))
}
// mixedVersionBackup is the struct that contains all the necessary
// state involved in the mixed-version backup test.
type mixedVersionBackup struct {
cluster cluster.Cluster
t test.Test
roachNodes option.NodeListOption
// backup collections that are created along the test
collections []*backupCollection
// databases where user data is being inserted
dbs []string
tables [][]string
tablesLoaded *atomic.Bool
// counter that is incremented atomically to provide unique
// identifiers to backups created during the test
currentBackupID int64