Skip to content

Commit 2768d79

Browse files
committed
metamorphic: use FS-based remote storage
The metamorphic test uses in-mem remote storage, which doesn't work when starting with an initial state (as in the crossversion tests). Note that there is code to save the remote storage contents to disk when the store is in-memory, but they are not read back when used as initial state. This commit changes to using FS-based remote storage in `shared` and `external` subdirs inside the data dir. Note that the store itself can still be in-memory; the data gets saved automatically with the store. We improve the FS-based Storage implementation to sync data and list objects. We can now allow simulating crashes in the metamorphic test when shared storage is enabled.
1 parent 4a2b3b8 commit 2768d79

File tree

3 files changed

+94
-97
lines changed

3 files changed

+94
-97
lines changed

metamorphic/options.go

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,12 @@ func parseOptions(
124124
return true
125125
case "TestOptions.shared_storage_enabled":
126126
opts.sharedStorageEnabled = true
127-
opts.sharedStorageFS = remote.NewInMem()
128127
if opts.Opts.Experimental.CreateOnShared == remote.CreateOnSharedNone {
129128
opts.Opts.Experimental.CreateOnShared = remote.CreateOnSharedAll
130129
}
131130
return true
132131
case "TestOptions.external_storage_enabled":
133132
opts.externalStorageEnabled = true
134-
opts.externalStorageFS = remote.NewInMem()
135133
return true
136134
case "TestOptions.secondary_cache_enabled":
137135
opts.secondaryCacheEnabled = true
@@ -226,7 +224,6 @@ func parseOptions(
226224
if opts.Opts.WALFailover != nil {
227225
opts.Opts.WALFailover.Secondary.FS = opts.Opts.FS
228226
}
229-
opts.InitRemoteStorageFactory()
230227
opts.Opts.EnsureDefaults()
231228
return err
232229
}
@@ -435,10 +432,8 @@ type TestOptions struct {
435432
asyncApplyToDB bool
436433
// Enable the use of shared storage.
437434
sharedStorageEnabled bool
438-
sharedStorageFS remote.Storage
439435
// Enable the use of shared storage for external file ingestion.
440436
externalStorageEnabled bool
441-
externalStorageFS remote.Storage
442437
// Enables the use of shared replication in TestOptions.
443438
useSharedReplicate bool
444439
// Enables the use of external replication in TestOptions.
@@ -473,20 +468,6 @@ type TestOptions struct {
473468
disableDownloads bool
474469
}
475470

476-
// InitRemoteStorageFactory initializes Opts.Experimental.RemoteStorage.
477-
func (testOpts *TestOptions) InitRemoteStorageFactory() {
478-
if testOpts.sharedStorageEnabled || testOpts.externalStorageEnabled {
479-
m := make(map[remote.Locator]remote.Storage)
480-
if testOpts.sharedStorageEnabled {
481-
m[""] = testOpts.sharedStorageFS
482-
}
483-
if testOpts.externalStorageEnabled {
484-
m["external"] = testOpts.externalStorageFS
485-
}
486-
testOpts.Opts.Experimental.RemoteStorage = remote.MakeSimpleFactory(m)
487-
}
488-
}
489-
490471
// CustomOption defines a custom option that configures the behavior of an
491472
// individual test run. Like all test options, custom options are serialized to
492473
// the OPTIONS file even if they're not options ordinarily understood by Pebble.
@@ -883,7 +864,6 @@ func RandomOptions(
883864
if testOpts.Opts.FormatMajorVersion < pebble.FormatMinForSharedObjects {
884865
testOpts.Opts.FormatMajorVersion = pebble.FormatMinForSharedObjects
885866
}
886-
testOpts.sharedStorageFS = remote.NewInMem()
887867
// If shared storage is enabled, pick between writing all files on shared
888868
// vs. lower levels only, 50% of the time.
889869
testOpts.Opts.Experimental.CreateOnShared = remote.CreateOnSharedAll
@@ -906,7 +886,6 @@ func RandomOptions(
906886
if testOpts.Opts.FormatMajorVersion < pebble.FormatSyntheticPrefixSuffix {
907887
testOpts.Opts.FormatMajorVersion = pebble.FormatSyntheticPrefixSuffix
908888
}
909-
testOpts.externalStorageFS = remote.NewInMem()
910889
}
911890

912891
// 75% of the time, randomize value separation parameters.
@@ -939,7 +918,6 @@ func RandomOptions(
939918
return testOpts.useDeleteOnlyCompactionExcises
940919
}
941920
testOpts.disableDownloads = rng.IntN(2) == 0
942-
testOpts.InitRemoteStorageFactory()
943921
testOpts.Opts.EnsureDefaults()
944922
return testOpts
945923
}

metamorphic/test.go

Lines changed: 41 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,10 @@
55
package metamorphic
66

77
import (
8-
"context"
98
"fmt"
109
"io"
1110
"os"
1211
"path"
13-
"path/filepath"
1412
"sort"
1513
"strings"
1614
"time"
@@ -108,12 +106,6 @@ func (t *Test) init(
108106
if numInstances < 1 {
109107
numInstances = 1
110108
}
111-
if t.testOpts.externalStorageEnabled {
112-
t.externalStorage = t.testOpts.externalStorageFS
113-
} else {
114-
t.externalStorage = remote.NewInMem()
115-
}
116-
117109
t.opsWaitOn, t.opsDone = computeSynchronizationPoints(t.ops)
118110

119111
if t.opts.Cache != nil {
@@ -192,10 +184,7 @@ func (t *Test) init(
192184
dir = path.Join(t.dir, fmt.Sprintf("db%d", i+1))
193185
}
194186
err = t.withRetries(func() error {
195-
// Give each DB its own CompactionScheduler.
196-
o := *t.opts
197-
o.Experimental.CompactionScheduler =
198-
pebble.NewConcurrencyLimitSchedulerWithNoPeriodicGrantingForTest()
187+
o := t.finalizeOptions(dir)
199188
db, err = pebble.Open(dir, &o)
200189
return err
201190
})
@@ -252,6 +241,45 @@ func (t *Test) init(
252241
return nil
253242
}
254243

244+
// finalizeOptions returns the options that need to be passed to pebble.Open().
245+
//
246+
// It initializes t.externalStorage and creates the compaction scheduler and
247+
// remote storage factory.
248+
func (t *Test) finalizeOptions(dataDir string) pebble.Options {
249+
o := *t.opts
250+
// Give each DB its own CompactionScheduler.
251+
o.Experimental.CompactionScheduler =
252+
pebble.NewConcurrencyLimitSchedulerWithNoPeriodicGrantingForTest()
253+
254+
// Set up external/shared storage.
255+
externalDir := o.FS.PathJoin(dataDir, "external")
256+
if err := o.FS.MkdirAll(externalDir, 0755); err != nil {
257+
panic(fmt.Sprintf("failed to create directory %q: %s", externalDir, err))
258+
}
259+
// Even if externalStorageEnabled is false, the test uses externalStorage to
260+
// emulate external ingestion.
261+
t.externalStorage = remote.NewLocalFS(externalDir, o.FS)
262+
263+
m := make(map[remote.Locator]remote.Storage)
264+
// If we are starting from an initial state (initialStatePath != ""), the
265+
// existing store might use shared or external storage, so we set them up
266+
// unconditionally.
267+
if t.testOpts.sharedStorageEnabled || t.testOpts.initialStatePath != "" {
268+
sharedDir := o.FS.PathJoin(dataDir, "shared")
269+
if err := o.FS.MkdirAll(sharedDir, 0755); err != nil {
270+
panic(fmt.Sprintf("failed to create directory %q: %s", sharedDir, err))
271+
}
272+
m[""] = remote.NewLocalFS(sharedDir, o.FS)
273+
}
274+
if t.testOpts.externalStorageEnabled || t.testOpts.initialStatePath != "" {
275+
m["external"] = t.externalStorage
276+
}
277+
if len(m) > 0 {
278+
o.Experimental.RemoteStorage = remote.MakeSimpleFactory(m)
279+
}
280+
return o
281+
}
282+
255283
func (t *Test) withRetries(fn func() error) error {
256284
return withRetries(fn, t.testOpts.RetryPolicy)
257285
}
@@ -279,13 +307,6 @@ func (t *Test) restartDB(dbID objID) error {
279307
if !t.testOpts.strictFS {
280308
return nil
281309
}
282-
if t.testOpts.sharedStorageEnabled {
283-
// We simulate a crash by essentially ignoring writes to disk after a
284-
// certain point. However, we cannot prevent the process (which didn't
285-
// actually crash) from deleting an external object before we call Close().
286-
// TODO(radu): perhaps we want all syncs to fail after the "crash" point?
287-
return nil
288-
}
289310
// We can't do this if we have more than one database since they share the
290311
// same FS (and we only close/reopen one of them).
291312
// TODO(radu): have each database use its own MemFS.
@@ -326,10 +347,7 @@ func (t *Test) restartDB(dbID objID) error {
326347
if len(t.dbs) > 1 {
327348
dir = path.Join(dir, fmt.Sprintf("db%d", dbID.slot()))
328349
}
329-
// Give each DB its own CompactionScheduler.
330-
o := *t.opts
331-
o.Experimental.CompactionScheduler =
332-
pebble.NewConcurrencyLimitSchedulerWithNoPeriodicGrantingForTest()
350+
o := t.finalizeOptions(dir)
333351
t.dbs[dbID.slot()-1], err = pebble.Open(dir, &o)
334352
if err != nil {
335353
return err
@@ -349,49 +367,6 @@ func (t *Test) saveInMemoryDataInternal() error {
349367
return err
350368
}
351369
}
352-
if t.testOpts.sharedStorageEnabled {
353-
if err := copyRemoteStorage(t.testOpts.sharedStorageFS, filepath.Join(t.dir, "shared")); err != nil {
354-
return err
355-
}
356-
}
357-
if t.testOpts.externalStorageEnabled {
358-
if err := copyRemoteStorage(t.testOpts.externalStorageFS, filepath.Join(t.dir, "external")); err != nil {
359-
return err
360-
}
361-
}
362-
return nil
363-
}
364-
365-
func copyRemoteStorage(fs remote.Storage, outputDir string) error {
366-
if err := vfs.Default.MkdirAll(outputDir, 0755); err != nil {
367-
return err
368-
}
369-
objs, err := fs.List("", "")
370-
if err != nil {
371-
return err
372-
}
373-
for i := range objs {
374-
reader, readSize, err := fs.ReadObject(context.TODO(), objs[i])
375-
if err != nil {
376-
return err
377-
}
378-
buf := make([]byte, readSize)
379-
if err := reader.ReadAt(context.TODO(), buf, 0); err != nil {
380-
return err
381-
}
382-
outputPath := vfs.Default.PathJoin(outputDir, objs[i])
383-
outputFile, err := vfs.Default.Create(outputPath, vfs.WriteCategoryUnspecified)
384-
if err != nil {
385-
return err
386-
}
387-
if _, err := outputFile.Write(buf); err != nil {
388-
outputFile.Close()
389-
return err
390-
}
391-
if err := outputFile.Close(); err != nil {
392-
return err
393-
}
394-
}
395370
return nil
396371
}
397372

objstorage/remote/localfs.go

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ package remote
77
import (
88
"context"
99
"io"
10-
"os"
1110
"path"
11+
"strings"
1212

13+
"github.com/cockroachdb/errors"
14+
"github.com/cockroachdb/errors/oserror"
1315
"github.com/cockroachdb/pebble/vfs"
1416
)
1517

@@ -78,24 +80,66 @@ func (r *localFSReader) Close() error {
7880
return nil
7981
}
8082

83+
func (f *localFSStore) sync() error {
84+
file, err := f.vfs.OpenDir(f.dirname)
85+
if err != nil {
86+
return err
87+
}
88+
return errors.CombineErrors(file.Sync(), file.Close())
89+
}
90+
91+
type objWriter struct {
92+
vfs.File
93+
store *localFSStore
94+
}
95+
96+
func (w *objWriter) Close() error {
97+
if w.File == nil {
98+
return nil
99+
}
100+
err := w.File.Sync()
101+
err = errors.CombineErrors(err, w.File.Close())
102+
err = errors.CombineErrors(err, w.store.sync())
103+
*w = objWriter{}
104+
return err
105+
}
106+
81107
// CreateObject is part of the remote.Storage interface.
82108
func (s *localFSStore) CreateObject(objName string) (io.WriteCloser, error) {
83109
file, err := s.vfs.Create(path.Join(s.dirname, objName), vfs.WriteCategoryUnspecified)
84-
return file, err
110+
if err != nil {
111+
return nil, err
112+
}
113+
return &objWriter{
114+
File: file,
115+
store: s,
116+
}, nil
85117
}
86118

87119
// List is part of the remote.Storage interface.
88120
func (s *localFSStore) List(prefix, delimiter string) ([]string, error) {
89-
// TODO(josh): For the intended use case of localfs.go of running 'pebble bench',
90-
// List can always return <nil, nil>, since this indicates a file has only one ref,
91-
// and since `pebble bench` implies running in a single-pebble-instance context.
92-
// https://github.com/cockroachdb/pebble/blob/a9a079d4fb6bf4a9ebc52e4d83a76ad4cbf676cb/objstorage/objstorageprovider/shared.go#L292
93-
return nil, nil
121+
if delimiter != "" {
122+
panic("delimiter unimplemented")
123+
}
124+
files, err := s.vfs.List(s.dirname)
125+
if err != nil {
126+
return nil, err
127+
}
128+
res := make([]string, 0, len(files))
129+
for _, name := range files {
130+
if strings.HasPrefix(name, prefix) {
131+
res = append(res, name)
132+
}
133+
}
134+
return res, nil
94135
}
95136

96137
// Delete is part of the remote.Storage interface.
97138
func (s *localFSStore) Delete(objName string) error {
98-
return s.vfs.Remove(path.Join(s.dirname, objName))
139+
if err := s.vfs.Remove(path.Join(s.dirname, objName)); err != nil {
140+
return err
141+
}
142+
return s.sync()
99143
}
100144

101145
// Size is part of the remote.Storage interface.
@@ -114,5 +158,5 @@ func (s *localFSStore) Size(objName string) (int64, error) {
114158

115159
// IsNotExistError is part of the remote.Storage interface.
116160
func (s *localFSStore) IsNotExistError(err error) bool {
117-
return err == os.ErrNotExist
161+
return oserror.IsNotExist(err)
118162
}

0 commit comments

Comments
 (0)