Skip to content

Commit 8704aac

Browse files
authored
Refactor validation logic and add verbose logging support (#13)
- Extract validate.go methods into smaller focused functions - Add input parameter validation and improved error handling - Optimize goroutine management with dedicated worker methods - Add verbose flag support to CLI and environment configuration - Add --worker count and --workload-duration as CLI parameters - Add --short-test flag for abbreviated test runs - Improve test configuration with better error reporting
1 parent 79c835c commit 8704aac

File tree

8 files changed

+378
-144
lines changed

8 files changed

+378
-144
lines changed

cmd/root.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"fmt"
2020
"log/slog"
2121
"os"
22+
"time"
2223

2324
"github.com/spf13/cobra"
2425

@@ -41,7 +42,7 @@ and integration with CockroachDB backup/restore workflows.
4142
It verifies that the storage provider is correctly configured,
4243
runs synthetic workloads, and produces network performance statistics.`,
4344
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
44-
if envConfig.DatabaseURL == "" {
45+
if envConfig.DatabaseURL == "" && !envConfig.Guess {
4546
return errors.New("database URL cannot be blank")
4647
}
4748
if envConfig.URI != "" {
@@ -74,7 +75,13 @@ func Execute() {
7475
f.StringVar(&envConfig.Path, "path", envConfig.Path, "destination path (e.g. bucket/folder)")
7576
f.StringVar(&envConfig.Endpoint, "endpoint", envConfig.Path, "http endpoint")
7677
f.StringVar(&envConfig.URI, "uri", envConfig.URI, "S3 URI")
78+
f.BoolVar(&envConfig.Guess, "guess", false, `perform a short test to guess suggested parameters:
79+
it only require access to the bucket;
80+
it does not try to run a full backup/restore cycle
81+
in the CockroachDB cluster.`)
7782
f.CountVarP(&verbosity, "verbosity", "v", "increase logging verbosity to debug")
83+
f.IntVar(&envConfig.Workers, "workers", 5, "number of concurrent workers")
84+
f.DurationVar(&envConfig.WorkloadDuration, "workload-duration", 5*time.Second, "duration of the workload")
7885
err := rootCmd.Execute()
7986

8087
if err != nil {

cmd/s3/s3.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ func command(env *env.Env) *cobra.Command {
3434
if err != nil {
3535
return err
3636
}
37+
if env.Guess {
38+
format.Report(cmd.OutOrStdout(), &validate.Report{
39+
SuggestedParams: store.Params(),
40+
})
41+
return nil
42+
}
3743
validator, err := validate.New(ctx, env, store)
3844
if err != nil {
3945
return err

internal/env/env.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,21 @@
1414

1515
package env
1616

17+
import "time"
18+
1719
// LookupEnv is a function that retrieves the value of an environment variable.
1820
type LookupEnv func(key string) (string, bool)
1921

2022
// Env holds the environment configuration.
2123
type Env struct {
22-
DatabaseURL string // the database connection URL
23-
Endpoint string // the S3 endpoint
24-
Path string // the S3 bucket path
25-
LookupEnv LookupEnv // allows injection of environment variable lookup for testing
26-
Testing bool // enables testing mode
27-
URI string // the S3 object URI (if not provided,will be constructed from Endpoint and Path)
28-
Verbose bool // enables verbose logging
24+
DatabaseURL string // the database connection URL
25+
Endpoint string // the S3 endpoint
26+
Guess bool // Guess the URL parameters, no validation.
27+
LookupEnv LookupEnv // allows injection of environment variable lookup for testing
28+
Path string // the S3 bucket path
29+
Testing bool // enables testing mode
30+
URI string // the S3 object URI (if not provided,will be constructed from Endpoint and Path)
31+
Verbose bool // enables verbose logging
32+
Workers int // number of concurrent workers
33+
WorkloadDuration time.Duration // duration to run the workload
2934
}

internal/validate/backup.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2025 Cockroach Labs, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package validate
16+
17+
import (
18+
"log/slog"
19+
20+
"github.com/jackc/pgx/v5/pgxpool"
21+
22+
"github.com/cockroachdb/errors"
23+
"github.com/cockroachdb/field-eng-powertools/stopper"
24+
"github.com/cockroachlabs-field/blobcheck/internal/db"
25+
)
26+
27+
// checkBackups verifies that there is exactly one full and one incremental backup.
28+
func (v *Validator) checkBackups(ctx *stopper.Context, extConn *db.ExternalConn) error {
29+
conn, err := v.acquireConn(ctx)
30+
if err != nil {
31+
return err
32+
}
33+
defer conn.Release()
34+
35+
backups, err := extConn.ListTableBackups(ctx, conn)
36+
if err != nil {
37+
return errors.Wrap(err, "failed to list table backups")
38+
}
39+
if len(backups) != expectedBackupCollections {
40+
return errors.Newf("expected exactly %d backup collection, got %d", expectedBackupCollections, len(backups))
41+
}
42+
43+
v.latest = backups[0]
44+
info, err := extConn.BackupInfo(ctx, conn, backups[0], v.sourceTable)
45+
if err != nil {
46+
return errors.Wrap(err, "failed to get backup info")
47+
}
48+
if len(info) != expectedBackupCount {
49+
return errors.Newf("expected exactly %d backups (1 full, 1 incremental), got %d backups", expectedBackupCount, len(info))
50+
}
51+
52+
fullCount := 0
53+
for _, i := range info {
54+
if i.Full {
55+
fullCount++
56+
}
57+
}
58+
if fullCount != expectedFullBackupCount {
59+
return errors.Newf("expected exactly %d full backup, got %d", expectedFullBackupCount, fullCount)
60+
}
61+
return nil
62+
}
63+
64+
// performRestore restores the backup to a separate database.
65+
func (v *Validator) performRestore(
66+
ctx *stopper.Context, conn *pgxpool.Conn, extConn *db.ExternalConn,
67+
) error {
68+
slog.Info("restoring backup")
69+
if err := v.restoredTable.Restore(ctx, conn, extConn, &v.sourceTable); err != nil {
70+
return errors.Wrap(err, "failed to restore backup")
71+
}
72+
return nil
73+
}
74+
75+
// runFullBackup runs a full backup in a separate database connection.
76+
func (v *Validator) runFullBackup(ctx *stopper.Context, extConn *db.ExternalConn) error {
77+
conn, err := v.acquireConn(ctx)
78+
if err != nil {
79+
return err
80+
}
81+
defer conn.Release()
82+
83+
slog.Info("starting full backup")
84+
if err := v.sourceTable.Backup(ctx, conn, extConn, false); err != nil {
85+
return errors.Wrap(err, "failed to create full backup")
86+
}
87+
return nil
88+
}
89+
90+
// runIncrementalBackup runs an incremental backup.
91+
func (v *Validator) runIncrementalBackup(
92+
ctx *stopper.Context, conn *pgxpool.Conn, extConn *db.ExternalConn,
93+
) error {
94+
slog.Info("starting incremental backup")
95+
if err := v.sourceTable.Backup(ctx, conn, extConn, true); err != nil {
96+
return errors.Wrap(err, "failed to create incremental backup")
97+
}
98+
return nil
99+
}
100+
101+
// verifyIntegrity checks that the restored data matches the original.
102+
func (v *Validator) verifyIntegrity(ctx *stopper.Context, conn *pgxpool.Conn) error {
103+
slog.Info("checking integrity")
104+
original, err := v.sourceTable.Fingerprint(ctx, conn)
105+
if err != nil {
106+
return errors.Wrap(err, "failed to get original table fingerprint")
107+
}
108+
109+
restore, err := v.restoredTable.Fingerprint(ctx, conn)
110+
if err != nil {
111+
return errors.Wrap(err, "failed to get restored table fingerprint")
112+
}
113+
114+
if original != restore {
115+
return errors.Errorf("integrity check failed: got %s, expected %s while comparing restored data with original",
116+
restore, original)
117+
}
118+
return nil
119+
}

internal/validate/db.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2025 Cockroach Labs, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package validate
16+
17+
import (
18+
"log/slog"
19+
20+
"github.com/jackc/pgx/v5/pgxpool"
21+
22+
"github.com/cockroachdb/errors"
23+
"github.com/cockroachdb/field-eng-powertools/stopper"
24+
"github.com/cockroachlabs-field/blobcheck/internal/db"
25+
)
26+
27+
// acquireConn acquires a database connection from the pool.
28+
func (v *Validator) acquireConn(ctx *stopper.Context) (*pgxpool.Conn, error) {
29+
conn, err := v.pool.Acquire(ctx)
30+
if err != nil {
31+
return nil, errors.Wrap(err, "failed to acquire database connection")
32+
}
33+
return conn, nil
34+
}
35+
36+
// createSourceTable creates the source database and table.
37+
func createSourceTable(ctx *stopper.Context, conn *pgxpool.Conn) (db.KvTable, error) {
38+
source := db.Database{Name: "_blobcheck"}
39+
if err := source.Create(ctx, conn); err != nil {
40+
return db.KvTable{}, errors.Wrap(err, "failed to create source database")
41+
}
42+
43+
// TODO (silvano): presplit table to have ranges in all nodes
44+
sourceTable := db.KvTable{
45+
Database: source,
46+
Schema: db.Public,
47+
Name: "mytable",
48+
}
49+
if err := sourceTable.Create(ctx, conn); err != nil {
50+
return db.KvTable{}, errors.Wrap(err, "failed to create source table")
51+
}
52+
return sourceTable, nil
53+
}
54+
55+
// createRestoredTable creates the restored database and table.
56+
func createRestoredTable(ctx *stopper.Context, conn *pgxpool.Conn) (db.KvTable, error) {
57+
dest := db.Database{Name: "_blobcheck_restored"}
58+
if err := dest.Create(ctx, conn); err != nil {
59+
return db.KvTable{}, errors.Wrap(err, "failed to create restored database")
60+
}
61+
62+
restoredTable := db.KvTable{
63+
Database: dest,
64+
Schema: db.Public,
65+
Name: "mytable",
66+
}
67+
return restoredTable, nil
68+
}
69+
70+
// captureInitialStats captures initial database statistics.
71+
func captureInitialStats(
72+
ctx *stopper.Context, extConn *db.ExternalConn, conn *pgxpool.Conn,
73+
) ([]*db.Stats, error) {
74+
slog.Info("capturing initial statistics")
75+
stats, err := extConn.Stats(ctx, conn)
76+
if err != nil {
77+
return nil, errors.Wrap(err, "failed to capture initial statistics")
78+
}
79+
return stats, nil
80+
}

internal/validate/minio_integration_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,11 +82,13 @@ func TestMinio(t *testing.T) {
8282

8383
bucketName := fmt.Sprintf("bucket-%d", time.Now().UnixMilli())
8484
var env = &env.Env{
85-
DatabaseURL: "postgresql://root@localhost:26257?sslmode=disable",
86-
Endpoint: endpoint,
87-
LookupEnv: lookup,
88-
Path: bucketName,
89-
Testing: true,
85+
DatabaseURL: "postgresql://root@localhost:26257?sslmode=disable",
86+
Endpoint: endpoint,
87+
LookupEnv: lookup,
88+
Path: bucketName,
89+
Testing: true,
90+
Workers: 5,
91+
WorkloadDuration: 5 * time.Second,
9092
}
9193
r.NoError(createMinioBucket(ctx, vars, env, bucketName))
9294
blobStorage, err := blob.S3FromEnv(ctx, env)

0 commit comments

Comments
 (0)