Skip to content

Commit 197d680

Browse files
authored
Merge pull request #1073 from percona/PBM-1455-oplog-restore-tests-with-mocks
PBM-1455: Oplog restore tests
2 parents 2bd080e + fc616b1 commit 197d680

File tree

148 files changed

+23521
-11742
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+23521
-11742
lines changed

go.mod

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ require (
1111
github.com/fsnotify/fsnotify v1.7.0
1212
github.com/golang/snappy v0.0.4
1313
github.com/google/uuid v1.6.0
14-
github.com/klauspost/compress v1.17.8
14+
github.com/klauspost/compress v1.17.11
1515
github.com/klauspost/pgzip v1.2.6
1616
github.com/mongodb/mongo-tools v0.0.0-20240723193119-837c2bc263f4
1717
github.com/pierrec/lz4 v2.6.1+incompatible
1818
github.com/pkg/errors v0.9.1
1919
github.com/spf13/cobra v1.8.1
2020
github.com/spf13/viper v1.19.0
21-
go.mongodb.org/mongo-driver v1.16.0
21+
go.mongodb.org/mongo-driver v1.17.1
2222
golang.org/x/mod v0.19.0
2323
golang.org/x/sync v0.10.0
2424
gopkg.in/yaml.v2 v2.4.0
@@ -30,6 +30,7 @@ require (
3030
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect
3131
github.com/alecthomas/units v0.0.0-20231202071711-9a357b53e9c9 // indirect
3232
github.com/containerd/log v0.1.0 // indirect
33+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
3334
github.com/distribution/reference v0.5.0 // indirect
3435
github.com/docker/go-connections v0.5.0 // indirect
3536
github.com/docker/go-units v0.5.0 // indirect
@@ -60,7 +61,7 @@ require (
6061
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
6162
github.com/xdg-go/scram v1.1.2 // indirect
6263
github.com/xdg-go/stringprep v1.0.4 // indirect
63-
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a // indirect
64+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
6465
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect
6566
go.opentelemetry.io/otel v1.24.0 // indirect
6667
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect

go.sum

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7
8282
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
8383
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
8484
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
85-
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
86-
github.com/klauspost/compress v1.17.8/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
85+
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
86+
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
8787
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
8888
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
8989
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -170,13 +170,13 @@ github.com/xdg-go/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY=
170170
github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4=
171171
github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8=
172172
github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM=
173-
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a h1:fZHgsYlfvtyqToslyjUt3VOPF4J7aK/3MPcK7xp3PDk=
174-
github.com/youmark/pkcs8 v0.0.0-20201027041543-1326539a0a0a/go.mod h1:ul22v+Nro/R083muKhosV54bj5niojjWZvU8xrevuH4=
173+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM=
174+
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI=
175175
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
176176
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
177177
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
178-
go.mongodb.org/mongo-driver v1.16.0 h1:tpRsfBJMROVHKpdGyc1BBEzzjDUWjItxbVSZ8Ls4BQ4=
179-
go.mongodb.org/mongo-driver v1.16.0/go.mod h1:oB6AhJQvFQL4LEHyXi6aJzQJtBiTQHiAd83l0GdFaiw=
178+
go.mongodb.org/mongo-driver v1.17.1 h1:Wic5cJIwJgSpBhe3lx3+/RybR5PiYRMpVFgO7cOHyIM=
179+
go.mongodb.org/mongo-driver v1.17.1/go.mod h1:wwWm/+BuOddhcq3n68LKRmgk2wXzmF6s0SFOa0GINL4=
180180
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk=
181181
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw=
182182
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
@@ -199,7 +199,6 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
199199
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
200200
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
201201
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
202-
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
203202
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
204203
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
205204
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=

pbm/oplog/db.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package oplog
2+
3+
import (
4+
"context"
5+
"strings"
6+
7+
"go.mongodb.org/mongo-driver/bson"
8+
"go.mongodb.org/mongo-driver/bson/primitive"
9+
"go.mongodb.org/mongo-driver/mongo"
10+
11+
"github.com/percona/percona-backup-mongodb/pbm/errors"
12+
)
13+
14+
// mDB represents MongoDB access functionality.
15+
type mDB struct {
16+
m *mongo.Client
17+
}
18+
19+
func newMDB(m *mongo.Client) *mDB {
20+
return &mDB{m: m}
21+
}
22+
23+
// getUUIDForNS ruturns UUID of existing collection.
24+
// When ns doesn't exist, it returns zero value without an error.
25+
// In case of error, it returns zero value for UUID in addition to error.
26+
func (d *mDB) getUUIDForNS(ctx context.Context, ns string) (primitive.Binary, error) {
27+
var uuid primitive.Binary
28+
29+
db, coll, _ := strings.Cut(ns, ".")
30+
cur, err := d.m.Database(db).ListCollections(ctx, bson.D{{"name", coll}})
31+
if err != nil {
32+
return uuid, errors.Wrap(err, "list collections")
33+
}
34+
defer cur.Close(ctx)
35+
36+
for cur.Next(ctx) {
37+
if subtype, data, ok := cur.Current.Lookup("info", "uuid").BinaryOK(); ok {
38+
uuid = primitive.Binary{
39+
Subtype: subtype,
40+
Data: data,
41+
}
42+
break
43+
}
44+
}
45+
46+
return uuid, errors.Wrap(cur.Err(), "list collections cursor")
47+
}
48+
49+
// ensureCollExists ensures that the collection exists before "creating" views or timeseries.
50+
// See PBM-921 for details.
51+
func (d *mDB) ensureCollExists(dbName string) error {
52+
err := d.m.Database(dbName).CreateCollection(context.TODO(), "system.views")
53+
if err != nil {
54+
// MongoDB 5.0 and 6.0 returns NamespaceExists error.
55+
// MongoDB 7.0 and 8.0 does not return error.
56+
// https://github.com/mongodb/mongo/blob/v6.0/src/mongo/base/error_codes.yml#L84
57+
const NamespaceExists = 48
58+
var cmdError mongo.CommandError
59+
if !errors.As(err, &cmdError) || cmdError.Code != NamespaceExists {
60+
return errors.Wrapf(err, "ensure %s.system.views collection", dbName)
61+
}
62+
}
63+
64+
return nil
65+
}
66+
67+
// applyOps is a wrapper for the applyOps database command, we pass in
68+
// a session to avoid opening a new connection for a few inserts at a time.
69+
func (d *mDB) applyOps(entries []interface{}) error {
70+
singleRes := d.m.Database("admin").RunCommand(context.TODO(), bson.D{{"applyOps", entries}})
71+
if err := singleRes.Err(); err != nil {
72+
return errors.Wrap(err, "applyOps")
73+
}
74+
res := bson.M{}
75+
err := singleRes.Decode(&res)
76+
if err != nil {
77+
return errors.Wrap(err, "decode singleRes")
78+
}
79+
if isFalsy(res["ok"]) {
80+
return errors.Errorf("applyOps command: %v", res["errmsg"])
81+
}
82+
83+
return nil
84+
}

pbm/oplog/db_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package oplog
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
8+
"go.mongodb.org/mongo-driver/bson"
9+
"go.mongodb.org/mongo-driver/bson/primitive"
10+
"go.mongodb.org/mongo-driver/mongo/integration/mtest"
11+
)
12+
13+
func TestGetUUIDForNS(t *testing.T) {
14+
mt := mtest.New(t, mtest.NewOptions().ClientType(mtest.Mock))
15+
16+
mt.Run("successful response from db", func(mt *mtest.T) {
17+
expectedUUID := primitive.Binary{Subtype: 0xFF, Data: []byte{0x01, 0x02, 0x03}}
18+
listCollRes := bson.D{
19+
{"name", "c1"},
20+
{"type", "collection"},
21+
{"info", bson.D{
22+
{"readOnly", false},
23+
{"uuid", expectedUUID},
24+
}},
25+
}
26+
mt.AddMockResponses(mtest.CreateCursorResponse(1, "mydb.c1", mtest.FirstBatch, listCollRes))
27+
28+
db := newMDB(mt.Client)
29+
uuid, err := db.getUUIDForNS(context.Background(), "mydb.c1")
30+
if err != nil {
31+
t.Errorf("got err=%v", err)
32+
}
33+
primitive.NewObjectID()
34+
35+
if !uuid.Equal(expectedUUID) {
36+
t.Errorf("wrong uuid for ns: expected=%v, got=%v", expectedUUID, uuid)
37+
}
38+
t.Log(uuid)
39+
})
40+
41+
mt.Run("failed response from db", func(mt *mtest.T) {
42+
errRes := mtest.CreateCommandErrorResponse(mtest.CommandError{
43+
Code: 11601,
44+
Name: "error",
45+
Message: "querying list collections",
46+
})
47+
mt.AddMockResponses(errRes)
48+
db := newMDB(mt.Client)
49+
_, err := db.getUUIDForNS(context.Background(), "mydb.c1")
50+
if err == nil {
51+
t.Error("expected to get error from getUUIDForNS")
52+
}
53+
if !strings.Contains(err.Error(), "list collections") {
54+
t.Error("wrong err")
55+
}
56+
})
57+
}

pbm/oplog/restore.go

Lines changed: 17 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,16 @@ func (c *cloneNS) SetNSPair(nsPair snapshot.CloneNS) {
118118
c.toDB, c.toColl = nsPair.SplitToNS()
119119
}
120120

121+
// mDBCl represents client interface for MongoDB logic used by OplogRestore
122+
type mDBCl interface {
123+
getUUIDForNS(ctx context.Context, ns string) (primitive.Binary, error)
124+
ensureCollExists(dbName string) error
125+
applyOps(entries []interface{}) error
126+
}
127+
121128
// OplogRestore is the oplog applyer
122129
type OplogRestore struct {
123-
dst *mongo.Client
130+
mdb mDBCl
124131
ver *db.Version
125132
needIdxWorkaround bool
126133
preserveUUIDopt bool
@@ -157,15 +164,15 @@ const saveLastDistTxns = 100
157164

158165
// NewOplogRestore creates an object for an oplog applying
159166
func NewOplogRestore(
160-
dst *mongo.Client,
167+
m *mongo.Client,
161168
ic *idx.IndexCatalog,
162169
sv *version.MongoVersion,
163170
unsafe,
164171
preserveUUID bool,
165172
ctxn chan phys.RestoreTxn,
166173
txnErr chan error,
167174
) (*OplogRestore, error) {
168-
m, err := ns.NewMatcher(append(snapshot.ExcludeFromRestore, excludeFromOplog...))
175+
matcher, err := ns.NewMatcher(append(snapshot.ExcludeFromRestore, excludeFromOplog...))
169176
if err != nil {
170177
return nil, errors.Wrap(err, "create matcher for the collections exclude")
171178
}
@@ -185,13 +192,13 @@ func NewOplogRestore(
185192
}
186193
ver := &db.Version{v[0], v[1], v[2]}
187194
return &OplogRestore{
188-
dst: dst,
195+
mdb: newMDB(m),
189196
ver: ver,
190197
preserveUUIDopt: preserveUUID,
191198
preserveUUID: preserveUUID,
192199
needIdxWorkaround: needsCreateIndexWorkaround(ver),
193200
indexCatalog: ic,
194-
excludeNS: m,
201+
excludeNS: matcher,
195202
noUUIDns: noUUID,
196203
txn: ctxn,
197204
txnSyncErr: txnErr,
@@ -298,7 +305,7 @@ func (o *OplogRestore) SetCloneNS(ctx context.Context, ns snapshot.CloneNS) erro
298305
o.cloneNS.SetNSPair(ns)
299306

300307
var err error
301-
o.cloneNS.toUUID, err = getUUIDForNS(ctx, o.dst, o.cloneNS.ToNS)
308+
o.cloneNS.toUUID, err = o.mdb.getUUIDForNS(ctx, o.cloneNS.ToNS)
302309
if err != nil {
303310
return errors.Wrap(err, "get to ns uuid")
304311
}
@@ -956,20 +963,14 @@ func (o *OplogRestore) handleNonTxnOp(op db.Oplog) error {
956963
}
957964
} else if op.Operation == "i" && collName == "system.views" {
958965
// PBM-921: ensure the collection exists before "creating" views or timeseries
959-
err := o.dst.Database(dbName).CreateCollection(context.TODO(), "system.views")
966+
err := o.mdb.ensureCollExists(dbName)
960967
if err != nil {
961-
// MongoDB 5.0 and 6.0 returns NamespaceExists error.
962-
// MongoDB 7.0 and 8.0 does not return error.
963-
// https://github.com/mongodb/mongo/blob/v6.0/src/mongo/base/error_codes.yml#L84
964-
const NamespaceExists = 48
965-
var cmdError mongo.CommandError
966-
if !errors.As(err, &cmdError) || cmdError.Code != NamespaceExists {
967-
return errors.Wrapf(err, "ensure %s.system.views collection", dbName)
968-
}
968+
return err
969969
}
970+
970971
}
971972

972-
err = o.applyOps([]interface{}{op})
973+
err = o.mdb.applyOps([]interface{}{op})
973974
if err != nil {
974975
// https://jira.percona.com/browse/PBM-818
975976
if o.unsafe && op.Namespace == "config.chunks" {
@@ -1071,25 +1072,6 @@ func extractIndexDocumentFromCommitIndexBuilds(op db.Oplog) (string, []*idx.Inde
10711072
return collectionName, nil
10721073
}
10731074

1074-
// applyOps is a wrapper for the applyOps database command, we pass in
1075-
// a session to avoid opening a new connection for a few inserts at a time.
1076-
func (o *OplogRestore) applyOps(entries []interface{}) error {
1077-
singleRes := o.dst.Database("admin").RunCommand(context.TODO(), bson.D{{"applyOps", entries}})
1078-
if err := singleRes.Err(); err != nil {
1079-
return errors.Wrap(err, "applyOps")
1080-
}
1081-
res := bson.M{}
1082-
err := singleRes.Decode(&res)
1083-
if err != nil {
1084-
return errors.Wrap(err, "decode singleRes")
1085-
}
1086-
if isFalsy(res["ok"]) {
1087-
return errors.Errorf("applyOps command: %v", res["errmsg"])
1088-
}
1089-
1090-
return nil
1091-
}
1092-
10931075
// filterUUIDs removes 'ui' entries from ops, including nested applyOps ops.
10941076
// It also modifies ops that rely on 'ui'.
10951077
func (o *OplogRestore) filterUUIDs(op db.Oplog) (db.Oplog, error) {
@@ -1261,29 +1243,3 @@ func isTruthy(val interface{}) bool {
12611243
func isFalsy(val interface{}) bool {
12621244
return !isTruthy(val)
12631245
}
1264-
1265-
// getUUIDForNS ruturns UUID of existing collection.
1266-
// When ns doesn't exist, it returns zero value without an error.
1267-
// In case of error, it returns zero value for UUID in addition to error.
1268-
func getUUIDForNS(ctx context.Context, m *mongo.Client, ns string) (primitive.Binary, error) {
1269-
var uuid primitive.Binary
1270-
1271-
d, c, _ := strings.Cut(ns, ".")
1272-
cur, err := m.Database(d).ListCollections(ctx, bson.D{{"name", c}})
1273-
if err != nil {
1274-
return uuid, errors.Wrap(err, "list collections")
1275-
}
1276-
defer cur.Close(ctx)
1277-
1278-
for cur.Next(ctx) {
1279-
if subtype, data, ok := cur.Current.Lookup("info", "uuid").BinaryOK(); ok {
1280-
uuid = primitive.Binary{
1281-
Subtype: subtype,
1282-
Data: data,
1283-
}
1284-
break
1285-
}
1286-
}
1287-
1288-
return uuid, errors.Wrap(cur.Err(), "list collections cursor")
1289-
}

0 commit comments

Comments
 (0)