288 lines
8.3 KiB
Go
288 lines
8.3 KiB
Go
// Copyright 2019 The LevelDB-Go and Pebble Authors. All rights reserved. Use
|
|
// of this source code is governed by a BSD-style license that can be found in
|
|
// the LICENSE file.
|
|
|
|
package pebble
|
|
|
|
import (
|
|
"os"
|
|
|
|
"github.com/cockroachdb/errors/oserror"
|
|
"github.com/cockroachdb/pebble/internal/base"
|
|
"github.com/cockroachdb/pebble/vfs"
|
|
"github.com/cockroachdb/pebble/vfs/atomicfs"
|
|
)
|
|
|
|
// checkpointOptions hold the optional parameters to construct checkpoint
|
|
// snapshots.
|
|
type checkpointOptions struct {
|
|
// flushWAL set to true will force a flush and sync of the WAL prior to
|
|
// checkpointing.
|
|
flushWAL bool
|
|
}
|
|
|
|
// CheckpointOption set optional parameters used by `DB.Checkpoint`.
|
|
type CheckpointOption func(*checkpointOptions)
|
|
|
|
// WithFlushedWAL enables flushing and syncing the WAL prior to constructing a
|
|
// checkpoint. This guarantees that any writes committed before calling
|
|
// DB.Checkpoint will be part of that checkpoint.
|
|
//
|
|
// Note that this setting can only be useful in cases when some writes are
|
|
// performed with Sync = false. Otherwise, the guarantee will already be met.
|
|
//
|
|
// Passing this option is functionally equivalent to calling
|
|
// DB.LogData(nil, Sync) right before DB.Checkpoint.
|
|
func WithFlushedWAL() CheckpointOption {
|
|
return func(opt *checkpointOptions) {
|
|
opt.flushWAL = true
|
|
}
|
|
}
|
|
|
|
// mkdirAllAndSyncParents creates destDir and any of its missing parents.
|
|
// Those missing parents, as well as the closest existing ancestor, are synced.
|
|
// Returns a handle to the directory created at destDir.
|
|
func mkdirAllAndSyncParents(fs vfs.FS, destDir string) (vfs.File, error) {
|
|
// Collect paths for all directories between destDir (excluded) and its
|
|
// closest existing ancestor (included).
|
|
var parentPaths []string
|
|
foundExistingAncestor := false
|
|
for parentPath := fs.PathDir(destDir); parentPath != "."; parentPath = fs.PathDir(parentPath) {
|
|
parentPaths = append(parentPaths, parentPath)
|
|
_, err := fs.Stat(parentPath)
|
|
if err == nil {
|
|
// Exit loop at the closest existing ancestor.
|
|
foundExistingAncestor = true
|
|
break
|
|
}
|
|
if !oserror.IsNotExist(err) {
|
|
return nil, err
|
|
}
|
|
}
|
|
// Handle empty filesystem edge case.
|
|
if !foundExistingAncestor {
|
|
parentPaths = append(parentPaths, "")
|
|
}
|
|
// Create destDir and any of its missing parents.
|
|
if err := fs.MkdirAll(destDir, 0755); err != nil {
|
|
return nil, err
|
|
}
|
|
// Sync all the parent directories up to the closest existing ancestor,
|
|
// included.
|
|
for _, parentPath := range parentPaths {
|
|
parentDir, err := fs.OpenDir(parentPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
err = parentDir.Sync()
|
|
if err != nil {
|
|
_ = parentDir.Close()
|
|
return nil, err
|
|
}
|
|
err = parentDir.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return fs.OpenDir(destDir)
|
|
}
|
|
|
|
// Checkpoint constructs a snapshot of the DB instance in the specified
|
|
// directory. The WAL, MANIFEST, OPTIONS, and sstables will be copied into the
|
|
// snapshot. Hard links will be used when possible. Beware of the significant
|
|
// space overhead for a checkpoint if hard links are disabled. Also beware that
|
|
// even if hard links are used, the space overhead for the checkpoint will
|
|
// increase over time as the DB performs compactions.
|
|
func (d *DB) Checkpoint(
|
|
destDir string, opts ...CheckpointOption,
|
|
) (
|
|
ckErr error, /* used in deferred cleanup */
|
|
) {
|
|
opt := &checkpointOptions{}
|
|
for _, fn := range opts {
|
|
fn(opt)
|
|
}
|
|
|
|
if _, err := d.opts.FS.Stat(destDir); !oserror.IsNotExist(err) {
|
|
if err == nil {
|
|
return &os.PathError{
|
|
Op: "checkpoint",
|
|
Path: destDir,
|
|
Err: oserror.ErrExist,
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
if opt.flushWAL && !d.opts.DisableWAL {
|
|
// Write an empty log-data record to flush and sync the WAL.
|
|
if err := d.LogData(nil /* data */, Sync); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Disable file deletions.
|
|
d.mu.Lock()
|
|
d.disableFileDeletions()
|
|
defer func() {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
d.enableFileDeletions()
|
|
}()
|
|
|
|
// TODO(peter): RocksDB provides the option to roll the manifest if the
|
|
// MANIFEST size is too large. Should we do this too?
|
|
|
|
// Lock the manifest before getting the current version. We need the
|
|
// length of the manifest that we read to match the current version that
|
|
// we read, otherwise we might copy a versionEdit not reflected in the
|
|
// sstables we copy/link.
|
|
d.mu.versions.logLock()
|
|
// Get the unflushed log files, the current version, and the current manifest
|
|
// file number.
|
|
memQueue := d.mu.mem.queue
|
|
current := d.mu.versions.currentVersion()
|
|
formatVers := d.mu.formatVers.vers
|
|
manifestFileNum := d.mu.versions.manifestFileNum
|
|
manifestSize := d.mu.versions.manifest.Size()
|
|
optionsFileNum := d.optionsFileNum
|
|
|
|
// Release the manifest and DB.mu so we don't block other operations on
|
|
// the database.
|
|
d.mu.versions.logUnlock()
|
|
d.mu.Unlock()
|
|
|
|
// Wrap the normal filesystem with one which wraps newly created files with
|
|
// vfs.NewSyncingFile.
|
|
fs := syncingFS{
|
|
FS: d.opts.FS,
|
|
syncOpts: vfs.SyncingFileOptions{
|
|
BytesPerSync: d.opts.BytesPerSync,
|
|
},
|
|
}
|
|
|
|
// Create the dir and its parents (if necessary), and sync them.
|
|
var dir vfs.File
|
|
defer func() {
|
|
if dir != nil {
|
|
_ = dir.Close()
|
|
}
|
|
if ckErr != nil {
|
|
// Attempt to cleanup on error.
|
|
paths, _ := fs.List(destDir)
|
|
for _, path := range paths {
|
|
_ = fs.Remove(path)
|
|
}
|
|
_ = fs.Remove(destDir)
|
|
}
|
|
}()
|
|
dir, ckErr = mkdirAllAndSyncParents(fs, destDir)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
|
|
{
|
|
// Link or copy the OPTIONS.
|
|
srcPath := base.MakeFilepath(fs, d.dirname, fileTypeOptions, optionsFileNum)
|
|
destPath := fs.PathJoin(destDir, fs.PathBase(srcPath))
|
|
ckErr = vfs.LinkOrCopy(fs, srcPath, destPath)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
}
|
|
|
|
{
|
|
// Set the format major version in the destination directory.
|
|
var versionMarker *atomicfs.Marker
|
|
versionMarker, _, ckErr = atomicfs.LocateMarker(fs, destDir, formatVersionMarkerName)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
|
|
// We use the marker to encode the active format version in the
|
|
// marker filename. Unlike other uses of the atomic marker,
|
|
// there is no file with the filename `formatVers.String()` on
|
|
// the filesystem.
|
|
ckErr = versionMarker.Move(formatVers.String())
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
ckErr = versionMarker.Close()
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
}
|
|
|
|
{
|
|
// Copy the MANIFEST, and create a pointer to it. We copy rather
|
|
// than link because additional version edits added to the
|
|
// MANIFEST after we took our snapshot of the sstables will
|
|
// reference sstables that aren't in our checkpoint. For a
|
|
// similar reason, we need to limit how much of the MANIFEST we
|
|
// copy.
|
|
srcPath := base.MakeFilepath(fs, d.dirname, fileTypeManifest, manifestFileNum)
|
|
destPath := fs.PathJoin(destDir, fs.PathBase(srcPath))
|
|
ckErr = vfs.LimitedCopy(fs, srcPath, destPath, manifestSize)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
|
|
// Recent format versions use an atomic marker for setting the
|
|
// active manifest. Older versions use the CURRENT file. The
|
|
// setCurrentFunc function will return a closure that will
|
|
// take the appropriate action for the database's format
|
|
// version.
|
|
var manifestMarker *atomicfs.Marker
|
|
manifestMarker, _, ckErr = atomicfs.LocateMarker(fs, destDir, manifestMarkerName)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
ckErr = setCurrentFunc(formatVers, manifestMarker, fs, destDir, dir)(manifestFileNum)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
ckErr = manifestMarker.Close()
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
}
|
|
|
|
// Link or copy the sstables.
|
|
for l := range current.Levels {
|
|
iter := current.Levels[l].Iter()
|
|
for f := iter.First(); f != nil; f = iter.Next() {
|
|
srcPath := base.MakeFilepath(fs, d.dirname, fileTypeTable, f.FileNum)
|
|
destPath := fs.PathJoin(destDir, fs.PathBase(srcPath))
|
|
ckErr = vfs.LinkOrCopy(fs, srcPath, destPath)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
}
|
|
}
|
|
|
|
// Copy the WAL files. We copy rather than link because WAL file recycling
|
|
// will cause the WAL files to be reused which would invalidate the
|
|
// checkpoint.
|
|
for i := range memQueue {
|
|
logNum := memQueue[i].logNum
|
|
if logNum == 0 {
|
|
continue
|
|
}
|
|
srcPath := base.MakeFilepath(fs, d.walDirname, fileTypeLog, logNum)
|
|
destPath := fs.PathJoin(destDir, fs.PathBase(srcPath))
|
|
ckErr = vfs.Copy(fs, srcPath, destPath)
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
}
|
|
|
|
// Sync and close the checkpoint directory.
|
|
ckErr = dir.Sync()
|
|
if ckErr != nil {
|
|
return ckErr
|
|
}
|
|
ckErr = dir.Close()
|
|
dir = nil
|
|
return ckErr
|
|
}
|