Skip to content

Commit b8b50a4

Browse files
kvserver: defer snapshot scratch cleanup to Store.Start (#171293)
kvserver: defer snapshot scratch cleanup to Store.Start
2 parents 3eb31da + 8a3094f commit b8b50a4

3 files changed

Lines changed: 98 additions & 7 deletions

File tree

pkg/kv/kvserver/store.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1705,17 +1705,14 @@ func NewStore(
17051705
})
17061706
s.eagerLeaseAcquisitionLimiter = cfg.EagerLeaseAcquisitionLimiter
17071707

1708-
// The snapshot storage is usually empty at this point since it is cleared
1709-
// after each snapshot application, except when the node crashed right before
1710-
// it can clean it up. If this fails it's not a correctness issue since the
1711-
// storage is also cleared before receiving a snapshot.
1708+
// The snapshot storage is constructed here but not yet cleared. With
1709+
// separated engines, leftover scratch files may be needed by WAG replay.
1710+
// The cleanup is deferred until after WAG replay is done and is flushed to
1711+
// guarantee that we don't need these files anymore.
17121712
//
17131713
// NB: we don't need the snapshot storage in the raft engine. With separated
17141714
// storage, the log engine part of snapshot ingestion is written as a batch.
17151715
s.sstSnapshotStorage = snaprecv.NewSSTSnapshotStorage(s.StateEngine(), s.limiters.BulkIOWriteRate)
1716-
if err := s.sstSnapshotStorage.Clear(); err != nil {
1717-
log.KvDistribution.Warningf(ctx, "failed to clear snapshot storage: %v", err)
1718-
}
17191716
s.protectedtsReader = cfg.ProtectedTimestampReader
17201717

17211718
s.limiters.ConcurrentAddSSTableRequests = limit.MakeConcurrentRequestLimiter(
@@ -2397,6 +2394,16 @@ func (s *Store) Start(ctx context.Context, stopper *stop.Stopper) error {
23972394
if err != nil {
23982395
return err
23992396
}
2397+
2398+
// Clear any leftover snapshot scratch files. Leftovers occur when a node
2399+
// crashed mid-snapshot; on a clean restart the directory is already empty.
2400+
if fn := s.cfg.TestingKnobs.BeforeClearSnapshotScratchOnStart; fn != nil {
2401+
fn()
2402+
}
2403+
if err := s.sstSnapshotStorage.Clear(); err != nil {
2404+
log.KvDistribution.Warningf(ctx, "failed to clear snapshot storage: %v", err)
2405+
}
2406+
24002407
logEvery := log.Every(10 * time.Second)
24012408
for i, repl := range repls {
24022409
// Log progress regularly, but not for the first replica (we only want to

pkg/kv/kvserver/store_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ import (
7373
"github.com/cockroachdb/cockroach/pkg/util/tracing"
7474
"github.com/cockroachdb/cockroach/pkg/util/uuid"
7575
"github.com/cockroachdb/errors"
76+
"github.com/cockroachdb/errors/oserror"
7677
"github.com/cockroachdb/redact"
7778
"github.com/stretchr/testify/assert"
7879
"github.com/stretchr/testify/require"
@@ -424,6 +425,86 @@ func TestStoreInitAndBootstrap(t *testing.T) {
424425
})
425426
}
426427

428+
// TestStoreStartClearsSnapshotStorageScratch verifies the scratch directory's
429+
// lifecycle across Store.Start, for both single and separated engines:
430+
//
431+
// 1. Leftover scratch files (simulating a crash mid-snapshot) survive past
432+
// the point where WAG replay would consume them. This is checked via the
433+
// BeforeClearSnapshotScratchOnStart knob.
434+
// 2. The same files are then removed by Clear before Start returns.
435+
//
436+
// TODO(sep-raft-log): Ensure that the test still passes after introducing WAG
437+
// replay. It is essential to have a flush after finishing WAG replay to avoid
438+
// deleting files that would be needed in case of a crash.
439+
func TestStoreStartClearsSnapshotStorageScratch(t *testing.T) {
440+
defer leaktest.AfterTest(t)()
441+
defer log.Scope(t).Close(t)
442+
443+
testutils.RunTrueAndFalse(t, "separated", func(t *testing.T, sepEng bool) {
444+
ctx := context.Background()
445+
stopper := stop.NewStopper()
446+
defer stopper.Stop(ctx)
447+
cfg := TestStoreConfig(nil)
448+
449+
// The knob fires during Start, by which point env and ssts (assigned
450+
// below after the store is built) are populated. The closure captures
451+
// them by reference.
452+
var env *fs.Env
453+
var ssts []string
454+
var preClearCalls int
455+
var preClearErrs []error
456+
cfg.TestingKnobs.BeforeClearSnapshotScratchOnStart = func() {
457+
preClearCalls++
458+
for _, p := range ssts {
459+
if _, statErr := env.Stat(p); statErr != nil {
460+
preClearErrs = append(preClearErrs,
461+
errors.Wrapf(statErr, "scratch file %s missing at pre-clear hook", p))
462+
}
463+
}
464+
}
465+
466+
store := createTestStoreWithoutStart(
467+
ctx, t, stopper, testStoreOpts{useSeparatedEngines: sepEng}, &cfg,
468+
)
469+
470+
// Seed a leftover scratch file under the snapshot storage directory.
471+
// We deliberately skip scratch.Close() to simulate a node that crashed
472+
// mid-snapshot and never ran the per-snapshot cleanup.
473+
scratch := store.sstSnapshotStorage.NewScratchSpace(
474+
roachpb.RangeID(42), uuid.MakeV4(), cfg.Settings,
475+
)
476+
f, err := scratch.NewFile(ctx, 0)
477+
require.NoError(t, err)
478+
require.NoError(t, f.Write([]byte("leftover sst")))
479+
require.NoError(t, f.Finish())
480+
481+
env = store.StateEngine().Env()
482+
ssts = scratch.SSTs()
483+
require.NotEmpty(t, ssts)
484+
485+
// Sanity check: the leftover file exists before Start.
486+
for _, p := range ssts {
487+
_, statErr := env.Stat(p)
488+
require.NoError(t, statErr, "scratch file %s should exist before Start", p)
489+
}
490+
491+
require.NoError(t, store.Start(ctx, stopper))
492+
store.WaitForInit()
493+
494+
// (1) The pre-clear hook ran exactly once, with all leftover files
495+
// still on disk. This is where WAG replay would consume them.
496+
require.Equal(t, 1, preClearCalls, "pre-clear knob should fire exactly once")
497+
require.Empty(t, preClearErrs, "leftover scratch files should survive past WAG replay")
498+
499+
// (2) Scratch file should have been removed by Start.
500+
for _, p := range ssts {
501+
_, statErr := env.Stat(p)
502+
require.True(t, oserror.IsNotExist(statErr),
503+
"scratch file %s should be removed by Start, got err=%v", p, statErr)
504+
}
505+
})
506+
}
507+
427508
// TestInitializeEngineErrors verifies bootstrap failure if engine
428509
// is not empty.
429510
func TestInitializeEngineErrors(t *testing.T) {

pkg/kv/kvserver/testing_knobs.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,9 @@ type StoreTestingKnobs struct {
361361
// HandleSnapshotDone is run after the entirety of receiving a snapshot,
362362
// regardless of whether it succeeds, gets cancelled, times out, or errors.
363363
HandleSnapshotDone func()
364+
// BeforeClearSnapshotScratchOnStart is called just before cleaning scratch
365+
// files on startup is executed.
366+
BeforeClearSnapshotScratchOnStart func()
364367
// ReplicaAddSkipLearnerRollback causes replica addition to skip the learner
365368
// rollback that happens when either the initial snapshot or the promotion of
366369
// a learner to a voter fails.

0 commit comments

Comments
 (0)