From 1f97424f312bbe67aa0a594e3c7d6b63ddc9f4da Mon Sep 17 00:00:00 2001 From: eleksir Date: Tue, 27 Feb 2024 00:34:44 +0300 Subject: [PATCH] [WIP] lint a bit, update deps --- .golangci.yml | 67 + config.go | 21 +- globals.go | 10 +- go.mod | 17 +- go.sum | 38 +- msg-parsers.go | 31 +- settings-db-util.go | 6 +- types.go | 6 +- util.go | 29 +- .../github.com/NicoNex/echotron/v3/types.go | 18 +- .../github.com/cockroachdb/pebble/.gitignore | 3 + vendor/github.com/cockroachdb/pebble/Makefile | 37 +- .../github.com/cockroachdb/pebble/README.md | 84 +- vendor/github.com/cockroachdb/pebble/batch.go | 317 +- .../cockroachdb/pebble/checkpoint.go | 52 +- .../github.com/cockroachdb/pebble/cleaner.go | 281 +- .../github.com/cockroachdb/pebble/commit.go | 61 +- .../cockroachdb/pebble/compaction.go | 1401 ++++---- .../cockroachdb/pebble/compaction_iter.go | 700 +++- .../cockroachdb/pebble/compaction_picker.go | 685 ++-- vendor/github.com/cockroachdb/pebble/db.go | 982 +++++- vendor/github.com/cockroachdb/pebble/event.go | 80 +- .../cockroachdb/pebble/external_iterator.go | 28 +- .../cockroachdb/pebble/filenames.go | 2 +- .../cockroachdb/pebble/flushable.go | 62 +- .../pebble/format_major_version.go | 106 +- .../github.com/cockroachdb/pebble/get_iter.go | 45 +- .../github.com/cockroachdb/pebble/ingest.go | 1458 +++++++- .../github.com/cockroachdb/pebble/internal.go | 3 +- .../pebble/internal/arenaskl/arena.go | 56 +- .../pebble/internal/arenaskl/iterator.go | 14 +- .../pebble/internal/arenaskl/node.go | 36 +- .../pebble/internal/arenaskl/skl.go | 53 +- .../pebble/internal/base/filenames.go | 89 +- .../pebble/internal/base/internal.go | 71 +- .../pebble/internal/base/metrics.go | 9 + .../pebble/internal/batchskl/skl.go | 15 +- .../pebble/internal/cache/clockpro.go | 208 +- .../pebble/internal/cache/entry.go | 8 +- .../pebble/internal/cache/robin_hood.go | 2 +- .../pebble/internal/cache/value.go | 2 +- .../pebble/internal/constants/constants.go | 17 + .../pebble/internal/humanize/humanize.go | 20 +- .../pebble/internal/invalidating/iter.go | 162 + .../pebble/internal/keyspan/defragment.go | 38 +- .../pebble/internal/keyspan/filter.go | 25 +- .../internal/keyspan/interleaving_iter.go | 925 ++--- .../pebble/internal/keyspan/iter.go | 4 +- .../pebble/internal/keyspan/level_iter.go | 4 +- .../pebble/internal/keyspan/merging_iter.go | 43 - .../pebble/internal/keyspan/transformer.go | 50 + .../pebble/internal/keyspan/truncate.go | 19 +- .../pebble/internal/manifest/btree.go | 34 +- .../internal/manifest/level_metadata.go | 77 +- .../pebble/internal/manifest/version.go | 464 ++- .../pebble/internal/manifest/version_edit.go | 403 ++- .../pebble/internal/private/sstable.go | 2 +- .../pebble/internal/rangekey/coalesce.go | 71 +- .../cockroachdb/pebble/internal/rate/rate.go | 387 --- .../pebble/internal/testkeys/testkeys.go | 229 +- .../cockroachdb/pebble/internal_iterator.go | 332 -- .../github.com/cockroachdb/pebble/iterator.go | 369 +- .../cockroachdb/pebble/level_checker.go | 18 +- .../cockroachdb/pebble/level_iter.go | 62 +- .../cockroachdb/pebble/log_recycler.go | 10 +- .../cockroachdb/pebble/mem_table.go | 104 +- .../cockroachdb/pebble/merging_iter.go | 10 +- .../github.com/cockroachdb/pebble/metrics.go | 413 ++- .../pebble/objstorage/noop_readahead.go | 19 +- .../pebble/objstorage/objstorage.go | 324 ++ .../objiotracing/obj_io_tracing.go | 68 + .../objiotracing/obj_io_tracing_off.go | 59 + .../objiotracing/obj_io_tracing_on.go | 410 +++ .../objstorage/objstorageprovider/provider.go | 522 +++ .../{ => objstorageprovider}/readahead.go | 49 +- .../objstorage/objstorageprovider/remote.go | 376 ++ .../objstorageprovider/remote_backing.go | 303 ++ .../objstorageprovider/remote_obj_name.go | 91 + .../objstorageprovider/remote_readable.go | 162 + .../remoteobjcat}/catalog.go | 137 +- .../remoteobjcat/version_edit.go | 254 ++ .../objstorageprovider/shared_writable.go | 65 + .../sharedcache/shared_cache.go | 900 +++++ .../{ => objstorageprovider}/vfs.go | 56 +- .../{ => objstorageprovider}/vfs_readable.go | 126 +- .../{ => objstorageprovider}/vfs_writable.go | 7 +- .../cockroachdb/pebble/objstorage/provider.go | 446 --- .../pebble/objstorage/remote/factory.go | 25 + .../pebble/objstorage/remote/localfs.go | 118 + .../objstorage/{shared => remote}/logging.go | 66 +- .../objstorage/{shared => remote}/mem.go | 57 +- .../pebble/objstorage/remote/storage.go | 126 + .../cockroachdb/pebble/objstorage/shared.go | 176 - .../pebble/objstorage/shared/storage.go | 61 - .../pebble/objstorage/shared_backing.go | 145 - .../pebble/objstorage/shared_readable.go | 97 - .../pebble/objstorage/shared_writable.go | 34 - .../objstorage/sharedobjcat/version_edit.go | 158 - vendor/github.com/cockroachdb/pebble/open.go | 224 +- .../github.com/cockroachdb/pebble/options.go | 232 +- vendor/github.com/cockroachdb/pebble/pacer.go | 179 +- .../cockroachdb/pebble/range_keys.go | 50 +- .../cockroachdb/pebble/rangekey/rangekey.go | 33 + .../cockroachdb/pebble/read_state.go | 20 +- .../cockroachdb/pebble/record/log_writer.go | 156 +- .../cockroachdb/pebble/scan_internal.go | 1007 ++++++ .../github.com/cockroachdb/pebble/snapshot.go | 419 ++- .../cockroachdb/pebble/sstable/block.go | 314 +- .../pebble/sstable/block_property.go | 30 +- .../cockroachdb/pebble/sstable/buffer_pool.go | 148 + .../cockroachdb/pebble/sstable/compression.go | 4 +- .../cockroachdb/pebble/sstable/filter.go | 36 +- .../cockroachdb/pebble/sstable/format.go | 163 +- .../cockroachdb/pebble/sstable/internal.go | 2 + .../cockroachdb/pebble/sstable/layout.go | 306 ++ .../cockroachdb/pebble/sstable/options.go | 34 + .../cockroachdb/pebble/sstable/properties.go | 275 +- .../cockroachdb/pebble/sstable/reader.go | 3073 ++--------------- .../cockroachdb/pebble/sstable/reader_iter.go | 291 ++ .../pebble/sstable/reader_iter_single_lvl.go | 1391 ++++++++ .../pebble/sstable/reader_iter_two_lvl.go | 1084 ++++++ .../pebble/sstable/reader_virtual.go | 206 ++ .../pebble/sstable/suffix_rewriter.go | 62 +- .../cockroachdb/pebble/sstable/table.go | 30 +- .../cockroachdb/pebble/sstable/value_block.go | 94 +- .../cockroachdb/pebble/sstable/writer.go | 320 +- .../cockroachdb/pebble/table_cache.go | 373 +- .../cockroachdb/pebble/table_stats.go | 348 +- .../cockroachdb/pebble/version_set.go | 286 +- ...efault_linux_noarm.go => default_linux.go} | 4 +- .../pebble/vfs/default_linux_arm.go | 73 - .../cockroachdb/pebble/vfs/disk_full.go | 161 +- .../cockroachdb/pebble/vfs/disk_health.go | 80 +- .../pebble/vfs/disk_usage_openbsd.go | 26 + .../cockroachdb/pebble/vfs/disk_usage_unix.go | 4 +- .../cockroachdb/pebble/vfs/logging_fs.go | 33 + .../cockroachdb/pebble/vfs/mem_fs.go | 115 +- .../cockroachdb/pebble/vfs/syncing_file.go | 36 +- .../github.com/cockroachdb/pebble/vfs/vfs.go | 18 + .../cockroachdb/tokenbucket/LICENSE | 201 ++ .../cockroachdb/tokenbucket/README.md | 2 + .../cockroachdb/tokenbucket/token_bucket.go | 202 ++ .../getsentry/sentry-go/CHANGELOG.md | 12 + .../getsentry/sentry-go/interfaces.go | 4 +- .../github.com/getsentry/sentry-go/scope.go | 2 +- .../github.com/getsentry/sentry-go/sentry.go | 2 +- .../getsentry/sentry-go/transport.go | 2 +- .../klauspost/compress/.goreleaser.yml | 4 - .../github.com/klauspost/compress/README.md | 26 +- vendor/github.com/klauspost/compress/s2sx.mod | 2 +- .../klauspost/compress/zstd/decodeheader.go | 56 +- .../klauspost/compress/zstd/enc_best.go | 37 +- .../compress/zstd/encoder_options.go | 6 +- .../klauspost/compress/zstd/frameenc.go | 2 +- .../compress/zstd/fse_decoder_generic.go | 11 +- .../klauspost/compress/zstd/seqdec_amd64.s | 136 +- .../prometheus/client_model/go/metrics.pb.go | 195 +- .../prometheus/common/expfmt/decode.go | 18 +- .../prometheus/common/expfmt/encode.go | 71 +- .../prometheus/common/expfmt/expfmt.go | 144 +- .../common/expfmt/openmetrics_create.go | 85 +- .../prometheus/common/expfmt/text_create.go | 118 +- .../prometheus/common/model/labels.go | 22 +- .../prometheus/common/model/metric.go | 362 +- .../x/exp/constraints/constraints.go | 50 + vendor/golang.org/x/sys/unix/mkerrors.sh | 2 +- vendor/golang.org/x/sys/unix/zerrors_linux.go | 36 +- .../x/sys/unix/zerrors_linux_386.go | 3 + .../x/sys/unix/zerrors_linux_amd64.go | 3 + .../x/sys/unix/zerrors_linux_arm.go | 3 + .../x/sys/unix/zerrors_linux_arm64.go | 3 + .../x/sys/unix/zerrors_linux_loong64.go | 3 + .../x/sys/unix/zerrors_linux_mips.go | 3 + .../x/sys/unix/zerrors_linux_mips64.go | 3 + .../x/sys/unix/zerrors_linux_mips64le.go | 3 + .../x/sys/unix/zerrors_linux_mipsle.go | 3 + .../x/sys/unix/zerrors_linux_ppc.go | 3 + .../x/sys/unix/zerrors_linux_ppc64.go | 3 + .../x/sys/unix/zerrors_linux_ppc64le.go | 3 + .../x/sys/unix/zerrors_linux_riscv64.go | 3 + .../x/sys/unix/zerrors_linux_s390x.go | 3 + .../x/sys/unix/zerrors_linux_sparc64.go | 3 + .../x/sys/unix/zsysnum_linux_386.go | 4 + .../x/sys/unix/zsysnum_linux_amd64.go | 3 + .../x/sys/unix/zsysnum_linux_arm.go | 4 + .../x/sys/unix/zsysnum_linux_arm64.go | 4 + .../x/sys/unix/zsysnum_linux_loong64.go | 4 + .../x/sys/unix/zsysnum_linux_mips.go | 4 + .../x/sys/unix/zsysnum_linux_mips64.go | 4 + .../x/sys/unix/zsysnum_linux_mips64le.go | 4 + .../x/sys/unix/zsysnum_linux_mipsle.go | 4 + .../x/sys/unix/zsysnum_linux_ppc.go | 4 + .../x/sys/unix/zsysnum_linux_ppc64.go | 4 + .../x/sys/unix/zsysnum_linux_ppc64le.go | 4 + .../x/sys/unix/zsysnum_linux_riscv64.go | 4 + .../x/sys/unix/zsysnum_linux_s390x.go | 4 + .../x/sys/unix/zsysnum_linux_sparc64.go | 4 + vendor/golang.org/x/sys/unix/ztypes_linux.go | 125 +- .../golang.org/x/sys/windows/env_windows.go | 17 +- .../x/sys/windows/syscall_windows.go | 3 +- vendor/modules.txt | 35 +- 201 files changed, 21474 insertions(+), 9282 deletions(-) create mode 100644 .golangci.yml create mode 100644 vendor/github.com/cockroachdb/pebble/internal/constants/constants.go create mode 100644 vendor/github.com/cockroachdb/pebble/internal/invalidating/iter.go create mode 100644 vendor/github.com/cockroachdb/pebble/internal/keyspan/transformer.go delete mode 100644 vendor/github.com/cockroachdb/pebble/internal/rate/rate.go delete mode 100644 vendor/github.com/cockroachdb/pebble/internal_iterator.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorage.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_off.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_on.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go rename vendor/github.com/cockroachdb/pebble/objstorage/{ => objstorageprovider}/readahead.go (80%) create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_backing.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_obj_name.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_readable.go rename vendor/github.com/cockroachdb/pebble/objstorage/{sharedobjcat => objstorageprovider/remoteobjcat}/catalog.go (73%) create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared_writable.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go rename vendor/github.com/cockroachdb/pebble/objstorage/{ => objstorageprovider}/vfs.go (60%) rename vendor/github.com/cockroachdb/pebble/objstorage/{ => objstorageprovider}/vfs_readable.go (62%) rename vendor/github.com/cockroachdb/pebble/objstorage/{ => objstorageprovider}/vfs_writable.go (87%) delete mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/provider.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/remote/factory.go create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/remote/localfs.go rename vendor/github.com/cockroachdb/pebble/objstorage/{shared => remote}/logging.go (57%) rename vendor/github.com/cockroachdb/pebble/objstorage/{shared => remote}/mem.go (69%) create mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/remote/storage.go delete mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/shared.go delete mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/shared/storage.go delete mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/shared_backing.go delete mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/shared_readable.go delete mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/shared_writable.go delete mode 100644 vendor/github.com/cockroachdb/pebble/objstorage/sharedobjcat/version_edit.go create mode 100644 vendor/github.com/cockroachdb/pebble/rangekey/rangekey.go create mode 100644 vendor/github.com/cockroachdb/pebble/scan_internal.go create mode 100644 vendor/github.com/cockroachdb/pebble/sstable/buffer_pool.go create mode 100644 vendor/github.com/cockroachdb/pebble/sstable/layout.go create mode 100644 vendor/github.com/cockroachdb/pebble/sstable/reader_iter.go create mode 100644 vendor/github.com/cockroachdb/pebble/sstable/reader_iter_single_lvl.go create mode 100644 vendor/github.com/cockroachdb/pebble/sstable/reader_iter_two_lvl.go create mode 100644 vendor/github.com/cockroachdb/pebble/sstable/reader_virtual.go rename vendor/github.com/cockroachdb/pebble/vfs/{default_linux_noarm.go => default_linux.go} (98%) delete mode 100644 vendor/github.com/cockroachdb/pebble/vfs/default_linux_arm.go create mode 100644 vendor/github.com/cockroachdb/pebble/vfs/disk_usage_openbsd.go create mode 100644 vendor/github.com/cockroachdb/tokenbucket/LICENSE create mode 100644 vendor/github.com/cockroachdb/tokenbucket/README.md create mode 100644 vendor/github.com/cockroachdb/tokenbucket/token_bucket.go create mode 100644 vendor/golang.org/x/exp/constraints/constraints.go diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..f241c16 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,67 @@ +run: + tests: false + skip-dirs: + - data + - vendor + modules-download-mode: readonly + allow-parallel-runners: true + go: '1.21' +output: + sort-results: true +linters-settings: + gosec: + excludes: + # Оно не используется для повышения безопасности или каких-то таких секъюрных вещей, оно используется для выборки + # произвольного элемента из массива, так что ИДИ НАХУЙ с этой проверкой и не еби мозги липовой безопасностью. + - G404 # Insecure random number source (rand) +linters: + disable-all: true + enable: + - errcheck + - gosimple + - govet + - ineffassign + - staticcheck + - unused + - asasalint + - asciicheck + - bodyclose + - containedctx + - contextcheck + - decorder + - durationcheck + - errchkjson + - errorlint + - exhaustive + - exportloopref + - gci + - ginkgolinter + - gocheckcompilerdirectives + - gocritic + - godot + - gofmt + - goimports + - gosec + - gosmopolitan + - grouper + - importas + - ireturn + - makezero + - mirror + - nakedret + - nilerr + - nilnil + - nlreturn + - noctx + - nolintlint + - nonamedreturns + - predeclared + - promlinter + - reassign + - revive + - rowserrcheck + - stylecheck + - unconvert + - wastedassign + - whitespace + - wsl diff --git a/config.go b/config.go index ce8e69e..26ecfe7 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "errors" "fmt" "os" @@ -21,7 +20,7 @@ func parseConfig(path string) (myConfig, error) { // Конфиг-файл длинноват для конфига, попробуем следующего кандидата. if fileInfo.Size() > 65535 { - err := errors.New(fmt.Sprintf("Config file %s is too long for config, skipping", path)) + err := fmt.Errorf("Config file %s is too long for config, skipping", path) return myConfig{}, err } @@ -36,8 +35,11 @@ func parseConfig(path string) (myConfig, error) { // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется. - var sampleConfig myConfig - var tmp map[string]interface{} + var ( + sampleConfig myConfig + tmp map[string]interface{} + ) + err = hjson.Unmarshal(buf, &tmp) // Не удалось распарсить. @@ -59,6 +61,7 @@ func parseConfig(path string) (myConfig, error) { // Валидируем значения из конфига. if sampleConfig.Redis.Server == "" { sampleConfig.Redis.Server = "localhost" + log.Infof("Redis server is not defined in config, using localhost") } @@ -67,19 +70,19 @@ func parseConfig(path string) (myConfig, error) { } if sampleConfig.Redis.Channel == "" { - err := errors.New(fmt.Sprintf("Channel field in config file %s must be set", path)) + err := fmt.Errorf("Channel field in config file %s must be set", path) return myConfig{}, err } if sampleConfig.Redis.MyChannel == "" { - err := errors.New(fmt.Sprintf("My_channel field in config file %s must be set", path)) + err := fmt.Errorf("My_channel field in config file %s must be set", path) return myConfig{}, err } if sampleConfig.Telegram.Token == "" { - return myConfig{}, errors.New(fmt.Sprintf("telegram.token field in config file %s must be set", path)) + return myConfig{}, fmt.Errorf("telegram.token field in config file %s must be set", path) } if sampleConfig.Loglevel == "" { @@ -89,7 +92,7 @@ func parseConfig(path string) (myConfig, error) { // sampleConfig.Log = "" if not set if sampleConfig.Csign == "" { - err := errors.New(fmt.Sprintf("Csign field in config file %s must be set", path)) + err := fmt.Errorf("Csign field in config file %s must be set", path) return myConfig{}, err } @@ -99,7 +102,7 @@ func parseConfig(path string) (myConfig, error) { } if sampleConfig.DataDir == "" { - return myConfig{}, errors.New(fmt.Sprintf("Data_dir field in config file %s must be set", path)) + return myConfig{}, fmt.Errorf("Data_dir field in config file %s must be set", path) } return sampleConfig, err diff --git a/globals.go b/globals.go index 539bef6..c3a0de0 100644 --- a/globals.go +++ b/globals.go @@ -9,13 +9,13 @@ import ( "github.com/go-redis/redis/v8" ) -// Config - это у нас глобальная штука :) +// Config - это у нас глобальная штука. var config myConfig -// To break circular message forwarding we must set some sane default, it can be overridden via config +// To break circular message forwarding we must set some sane default, it can be overridden via config. var forwardMax int64 = 5 -// Объектики клиента-редиски +// Объектики клиента-редиски. var redisClient *redis.Client var subscriber *redis.PubSub @@ -25,10 +25,10 @@ var ctx = context.Background() // Ставится в true, если мы получили сигнал на выключение var shutdown = false -// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов +// Канал, в который приходят уведомления для хэндлера сигналов от траппера сигналов. var sigChan = make(chan os.Signal, 1) -// Мапка с открытыми дескрипторами баз с настройками +// Мапка с открытыми дескрипторами баз с настройками. var settingsDB = make(map[string]*pebble.DB) var tg echotron.API diff --git a/go.mod b/go.mod index 507b125..488652d 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,9 @@ module aleesa-telegram-go go 1.19 require ( - github.com/NicoNex/echotron/v3 v3.28.0 + github.com/NicoNex/echotron/v3 v3.29.0 github.com/cockroachdb/errors v1.11.1 - github.com/cockroachdb/pebble v1.0.0 + github.com/cockroachdb/pebble v1.1.0 github.com/davecgh/go-spew v1.1.1 github.com/go-redis/redis/v8 v8.11.5 github.com/hjson/hjson-go v3.3.0+incompatible @@ -18,21 +18,22 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b // indirect github.com/cockroachdb/redact v1.1.5 // indirect + github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/getsentry/sentry-go v0.26.0 // indirect + github.com/getsentry/sentry-go v0.27.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/klauspost/compress v1.17.4 // indirect + github.com/klauspost/compress v1.17.7 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.46.0 // indirect + github.com/prometheus/client_model v0.6.0 // indirect + github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect - golang.org/x/exp v0.0.0-20240119083558-1b970713d09a // indirect - golang.org/x/sys v0.16.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.32.0 // indirect ) diff --git a/go.sum b/go.sum index 7e370ff..f6fd7a5 100644 --- a/go.sum +++ b/go.sum @@ -1,20 +1,22 @@ github.com/DataDog/zstd v1.5.5 h1:oWf5W7GtOLgp6bciQYDmhHHjdhYkALu6S/5Ni9ZgSvQ= github.com/DataDog/zstd v1.5.5/go.mod h1:g4AWEaM3yOg3HYfnJ3YIawPnVdXJh9QME85blwSAmyw= -github.com/NicoNex/echotron/v3 v3.28.0 h1:A5uGWlD1iQYJe41XYxmb/sAQ8ZoDSJgbB2+Ix9PDimY= -github.com/NicoNex/echotron/v3 v3.28.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI= +github.com/NicoNex/echotron/v3 v3.29.0 h1:STzeYNauF+h82vTL9QIqIBEvhke/bsxgYKQMCmJKnLM= +github.com/NicoNex/echotron/v3 v3.29.0/go.mod h1:LpP5IyHw0y+DZUZMBgXEDAF9O8feXrQu7w7nlJzzoZI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cockroachdb/datadriven v1.0.3-0.20230801171734-e384cf455877 h1:1MLK4YpFtIEo3ZtMA5C795Wtv5VuUnrXX7mQG+aHg6o= +github.com/cockroachdb/datadriven v1.0.3-0.20230413201302-be42291fc80f h1:otljaYPt5hWxV3MUfO5dFPFiOXg9CyG5/kCfayTqsJ4= github.com/cockroachdb/errors v1.11.1 h1:xSEW75zKaKCWzR3OfxXUxgrk/NtT4G1MiOv5lWZazG8= github.com/cockroachdb/errors v1.11.1/go.mod h1:8MUxA3Gi6b25tYlFEBGLf+D8aISL+M4MIpiWMSNRfxw= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= -github.com/cockroachdb/pebble v1.0.0 h1:WZWlV/s78glZbY2ylUITDOWSVBD3cLjcWPLRPFbHNYg= -github.com/cockroachdb/pebble v1.0.0/go.mod h1:bynZ3gvVyhlvjLI7PT6dmZ7g76xzJ7HpxfjgkzCGz6s= +github.com/cockroachdb/pebble v1.1.0 h1:pcFh8CdCIt2kmEpK0OIatq67Ln9uGDYY3d5XnE0LJG4= +github.com/cockroachdb/pebble v1.1.0/go.mod h1:sEHm5NOXxyiAoKWhoFxT8xMgd/f3RA6qUqQ1BXKrh2E= github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -22,8 +24,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/getsentry/sentry-go v0.26.0 h1:IX3++sF6/4B5JcevhdZfdKIHfyvMmAq/UnqcyT2H6mA= -github.com/getsentry/sentry-go v0.26.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= @@ -31,13 +33,13 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/hjson/hjson-go v3.3.0+incompatible h1:Rqr+Ya+0aCJMjaE4s8E9YKvuJLuLVpEvz4ONum52vnI= github.com/hjson/hjson-go v3.3.0+incompatible/go.mod h1:qsetwF8NlsTsOTwZTApNlTCerV+b2GjYRRcIk4JMFio= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= -github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg= +github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -53,10 +55,10 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= github.com/prometheus/client_golang v1.18.0/go.mod h1:T+GXkCk5wSJyOqMIzVgvvjFDlkOQntgjkJWKrN5txjA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.46.0 h1:doXzt5ybi1HBKpsZOL0sSkaNHJJqkyfEWZGGqqScV0Y= -github.com/prometheus/common v0.46.0/go.mod h1:Tp0qkxpb9Jsg54QMe+EAmqXkSV7Evdy1BTn+g2pa/hQ= +github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos= +github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -72,8 +74,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA= -golang.org/x/exp v0.0.0-20240119083558-1b970713d09a/go.mod h1:idGWGoKP1toJGkd5/ig9ZLuPcZBC3ewk7SzmH0uou08= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -89,8 +91,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= diff --git a/msg-parsers.go b/msg-parsers.go index 75be0c6..5cf955d 100644 --- a/msg-parsers.go +++ b/msg-parsers.go @@ -11,10 +11,10 @@ import ( // telego msg parser -// redisMsgParser парсит json-чики прилетевшие из REDIS-ки, причём, json-чики должны быть относительно валидными +// redisMsgParser парсит json-чики прилетевшие из REDIS-ки, причём, json-чики должны быть относительно валидными. func redisMsgParser(msg string) { if shutdown { - // Если мы завершаем работу программы, то нам ничего обрабатывать не надо + // Если мы завершаем работу программы, то нам ничего обрабатывать не надо. return } @@ -24,48 +24,56 @@ func redisMsgParser(msg string) { if err := json.Unmarshal([]byte(msg), &j); err != nil { log.Warnf("Unable to to parse message from redis channel: %s", err) + return } // Validate our j if exist := j.From; exist == "" { log.Warnf("Incorrect msg from redis, no from field: %s", msg) + return } if exist := j.Chatid; exist == "" { log.Warnf("Incorrect msg from redis, no chatid field: %s", msg) + return } if exist := j.Userid; exist == "" { log.Warnf("Incorrect msg from redis, no userid field: %s", msg) + return } if exist := j.Message; exist == "" { log.Warnf("Incorrect msg from redis, no message field: %s", msg) + return } if exist := j.Plugin; exist == "" { log.Warnf("Incorrect msg from redis, no plugin field: %s", msg) + return } if exist := j.Mode; exist == "" { log.Warnf("Incorrect msg from redis, no mode field: %s", msg) + return } // j.Misc.Answer может и не быть, тогда ответа на такое сообщение не будет if j.Misc.Answer == 0 { log.Debug("Field Misc->Answer = 0, skipping message") + return } // j.Misc.BotNick тоже можно не передавать, тогда будет записана пустая строка - // j.Misc.CSign если нам его не передали, возьмём значение из конфига + // j.Misc.CSign если нам его не передали, возьмём значение из конфига. (но по идее нам оно тут не нужнО.) if exist := j.Misc.Csign; exist == "" { j.Misc.Csign = config.Csign } @@ -82,6 +90,7 @@ func redisMsgParser(msg string) { // Отвалидировались, теперь вернёмся к нашим баранам. var opts *echotron.MessageOptions + chatid, err := strconv.ParseInt(j.Chatid, 10, 64) if err != nil { @@ -100,8 +109,6 @@ func redisMsgParser(msg string) { log.Errorf("Unable to send message to telegram api: %s", err) log.Errorf("Response dump: %s", spew.Sdump(resp)) } - - return } // telegramMsgParser парсит ивент, прилетевший из bot api. @@ -128,11 +135,14 @@ func telegramMsgParser(msg *echotron.Update) { // TODO: проверить, надо ли прощаться с ушедшим участником чата и попрощаться, если надо } + var rmsg rMsg + switch msg.Message.Chat.Type { case "private": // handle private messages - // всегда ставим флажок, что надо отвечать. + rmsg.Misc.Answer = 1 // если фраза является командой - засылаем её в парсер команд + // Засылаем фразу в misc-канал (в роутер) case "group", "supergroup": // handle public (super)group messages @@ -144,11 +154,14 @@ func telegramMsgParser(msg *echotron.Update) { // Здесь же, пытаемся убрать из фразы ник или имя бота // Засылаем фразу в misc-канал (в роутер) case "channel": - // Тут мы ничего сделать не можем + // Тут мы ничего сделать не можем. + } //nolint:wsl +} - } +func cmdParser(cmd string) error { + var err error - return + return err } /* vim: set ft=go noet ai ts=4 sw=4 sts=4: */ diff --git a/settings-db-util.go b/settings-db-util.go index 58763c9..e56801d 100644 --- a/settings-db-util.go +++ b/settings-db-util.go @@ -11,7 +11,7 @@ import ( log "github.com/sirupsen/logrus" ) -// StoreKV сохраняет в указанной бд ключ и значение +// StoreKV сохраняет в указанной бд ключ и значение. func StoreKV(db *pebble.DB, key string, value string) error { var kArray = []byte(key) var vArray = []byte(value) @@ -40,7 +40,7 @@ func FetchV(db *pebble.DB, key string) (string, error) { return valueString, err } -// Достанем настройку из БД с настройками +// getSetting достаёт настройку из БД с настройками. func getSetting(chatID string, setting string) string { var err error @@ -82,7 +82,7 @@ func getSetting(chatID string, setting string) string { return value } -// Сохраним настройку в БД с настройками +// saveSetting сохраняет настройку в БД с настройками func saveSetting(chatID string, setting string, value string) error { var chatHash = sha256.Sum256([]byte(chatID)) var database = fmt.Sprintf("settings_db/%x", chatHash) diff --git a/types.go b/types.go index 269391e..b118ce8 100644 --- a/types.go +++ b/types.go @@ -1,6 +1,6 @@ package main -// Конфиг +// myConfig структура, описывающая формат Конфига. type myConfig struct { Redis struct { Server string `json:"server,omitempty"` @@ -20,7 +20,7 @@ type myConfig struct { DataDir string `json:"data_dir,omitempty"` } -// Входящее сообщение из pubsub-канала redis-ки +// rMsg структура, описывающая формат входящего сообщения из pubsub-канала redis-ки. type rMsg struct { From string `json:"from,omitempty"` Chatid string `json:"chatid,omitempty"` @@ -39,7 +39,7 @@ type rMsg struct { } `json:"Misc"` } -// Исходящее сообщение в pubsub-канал redis-ки +// sMsg структура, описывающая исходящее сообщение в pubsub-канал redis-ки. type sMsg struct { From string `json:"from"` Chatid string `json:"chatid"` diff --git a/util.go b/util.go index aa3a333..a60ffc9 100644 --- a/util.go +++ b/util.go @@ -3,7 +3,6 @@ package main import ( "encoding/json" "fmt" - "io/ioutil" "os" "path/filepath" "syscall" @@ -12,6 +11,7 @@ import ( log "github.com/sirupsen/logrus" ) +// readConfig считывает и парсит конфиг-файл. func readConfig() { configLoaded := false executablePath, err := os.Executable() @@ -40,27 +40,33 @@ func readConfig() { // Конфиг-файл длинноват для конфига, попробуем следующего кандидата if fileInfo.Size() > 65535 { log.Warnf("Config file %s is too long for config, skipping", location) + continue } - buf, err := ioutil.ReadFile(location) + buf, err := os.ReadFile(location) // Не удалось прочитать, попробуем следующего кандидата if err != nil { log.Warnf("Skip reading config file %s: %s", location, err) + continue } // Исходя из документации, hjson какбы умеет парсить "кривой" json, но парсит его в map-ку. // Интереснее на выходе получить структурку: то есть мы вначале конфиг преобразуем в map-ку, затем эту map-ку // сериализуем в json, а потом json преврщааем в стркутурку. Не очень эффективно, но он и не часто требуется. - var sampleConfig myConfig - var tmp map[string]interface{} + var ( + sampleConfig myConfig + tmp map[string]interface{} + ) + err = hjson.Unmarshal(buf, &tmp) // Не удалось распарсить - попробуем следующего кандидата if err != nil { log.Warnf("Skip parsing config file %s: %s", location, err) + continue } @@ -69,17 +75,20 @@ func readConfig() { // Не удалось преобразовать map-ку в json if err != nil { log.Warnf("Skip parsing config file %s: %s", location, err) + continue } if err := json.Unmarshal(tmpjson, &sampleConfig); err != nil { log.Warnf("Skip parsing config file %s: %s", location, err) + continue } - // Валидируем значения из конфига + // Валидируем значения из конфига. if sampleConfig.Redis.Server == "" { sampleConfig.Redis.Server = "localhost" + log.Infof("Redis server is not defined in config, using localhost") } @@ -119,7 +128,9 @@ func readConfig() { config = sampleConfig configLoaded = true + log.Infof("Using %s as config file", location) + break } @@ -129,7 +140,7 @@ func readConfig() { } } -// Хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения +// sigHandler хэндлер сигналов закрывает все бд, все сетевые соединения и сваливает из приложения. func sigHandler() { var err error @@ -148,10 +159,10 @@ func sigHandler() { continue } - // Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true + // Чтобы не срать в логи ошибками от редиски, проставим shutdown state приложения в true. shutdown = true - // Отпишемся от всех каналов и закроем коннект к редиске + // Отпишемся от всех каналов и закроем коннект к редиске. if err = subscriber.Unsubscribe(ctx); err != nil { log.Errorf("Unable to unsubscribe from redis channels cleanly: %s", err) } else { @@ -164,7 +175,7 @@ func sigHandler() { log.Debug("Close redis connection") } - // close all telego stuff + // close all telego stuff. if len(settingsDB) > 0 { log.Debug("Closing runtime irc channel settings db") diff --git a/vendor/github.com/NicoNex/echotron/v3/types.go b/vendor/github.com/NicoNex/echotron/v3/types.go index f0f75e3..f3bf191 100644 --- a/vendor/github.com/NicoNex/echotron/v3/types.go +++ b/vendor/github.com/NicoNex/echotron/v3/types.go @@ -489,11 +489,13 @@ type Chat struct { InviteLink string `json:"invite_link,omitempty"` EmojiStatusCustomEmojiID string `json:"emoji_status_custom_emoji_id,omitempty"` Type string `json:"type"` + CustomEmojiStickerSetName string `json:"custom_emoji_sticker_set_name,omitempty"` AccentColorID int `json:"accent_color_id,omitempty"` ProfileAccentColorID int `json:"profile_accent_color_id,omitempty"` EmojiStatusExpirationDate int `json:"emoji_status_expiration_date,omitempty"` MessageAutoDeleteTime int `json:"message_auto_delete_time,omitempty"` SlowModeDelay int `json:"slow_mode_delay,omitempty"` + UnrestrictBoostCount int `json:"unrestrict_boost_count,omitempty"` LinkedChatID int64 `json:"linked_chat_id,omitempty"` ID int64 `json:"id"` IsForum bool `json:"is_forum,omitempty"` @@ -553,10 +555,12 @@ type Message struct { UsersShared *UsersShared `json:"users_shared,omitempty"` ChatShared *ChatShared `json:"chat_shared,omitempty"` Story *Story `json:"story,omitempty"` + ReplyToStory *Story `json:"reply_to_story,omitempty"` ExternalReply *ExternalReplyInfo `json:"external_reply,omitempty"` Quote *TextQuote `json:"quote,omitempty"` LinkPreviewOptions *LinkPreviewOptions `json:"link_preview_options,omitempty"` ForwardOrigin *MessageOrigin `json:"forward_origin,omitempty"` + BoostAdded *ChatBoostAdded `json:"boost_added,omitempty"` MediaGroupID string `json:"media_group_id,omitempty"` ConnectedWebsite string `json:"connected_website,omitempty"` NewChatTitle string `json:"new_chat_title,omitempty"` @@ -575,6 +579,7 @@ type Message struct { Date int `json:"date"` MigrateToChatID int `json:"migrate_to_chat_id,omitempty"` EditDate int `json:"edit_date,omitempty"` + SenderBoostCount int `json:"sender_boost_count,omitempty"` DeleteChatPhoto bool `json:"delete_chat_photo,omitempty"` IsTopicMessage bool `json:"is_topic_message,omitempty"` IsAutomaticForward bool `json:"is_automatic_forward,omitempty"` @@ -1173,6 +1178,11 @@ type ChatJoinRequest struct { UserChatID int64 `json:"user_chat_id"` } +// ChatBoostAdded represents a service message about a user boosting a chat. +type ChatBoostAdded struct { + BoostCount int `json:"boost_count"` +} + // ForumTopicCreated represents a service message about a new forum topic created in the chat. type ForumTopicCreated struct { Name string `json:"name"` @@ -1238,9 +1248,11 @@ type ChatShared struct { ChatID int64 `json:"chat_id"` } -// Story represents a message about a forwarded story in the chat. -// Currently holds no information. -type Story struct{} +// Story represents a story. +type Story struct { + Chat Chat `json:"chat"` + ID int64 `json:"id"` +} type ReactionType interface { ImplementsReactionType() diff --git a/vendor/github.com/cockroachdb/pebble/.gitignore b/vendor/github.com/cockroachdb/pebble/.gitignore index 73c80d7..87ef192 100644 --- a/vendor/github.com/cockroachdb/pebble/.gitignore +++ b/vendor/github.com/cockroachdb/pebble/.gitignore @@ -1,6 +1,9 @@ +# Github action artifacts. +artifacts # Profiling artifacts. cpu.*.prof heap.prof mutex.prof +coverprofile.out # Testing artifacts meta.*.test diff --git a/vendor/github.com/cockroachdb/pebble/Makefile b/vendor/github.com/cockroachdb/pebble/Makefile index 78c9ba3..2cbf3ea 100644 --- a/vendor/github.com/cockroachdb/pebble/Makefile +++ b/vendor/github.com/cockroachdb/pebble/Makefile @@ -4,7 +4,8 @@ GOFLAGS := STRESSFLAGS := TAGS := invariants TESTS := . -LATEST_RELEASE := $(shell git fetch origin && git branch -r --list '*/crl-release-*' | grep -o 'crl-release-.*$$' | sort | tail -1) +PREV_RELEASE := crl-release-23.1 +COVER_PROFILE := coverprofile.out .PHONY: all all: @@ -15,7 +16,10 @@ all: @echo " make stressrace" @echo " make stressmeta" @echo " make crossversion-meta" + @echo " make testcoverage" @echo " make mod-update" + @echo " make generate" + @echo " make generate-test-data" @echo " make clean" override testflags := @@ -23,6 +27,10 @@ override testflags := test: ${GO} test -tags '$(TAGS)' ${testflags} -run ${TESTS} ${PKG} +.PHONY: testcoverage +testcoverage: + ${GO} test -tags '$(TAGS)' ${testflags} -run ${TESTS} ${PKG} -coverprofile ${COVER_PROFILE} + .PHONY: testrace testrace: testflags += -race -timeout 20m testrace: test @@ -34,6 +42,10 @@ testmsan: export CC=clang testmsan: testflags += -msan -timeout 20m testmsan: test +.PHONY: testobjiotracing +testobjiotracing: + ${GO} test -tags '$(TAGS) pebble_obj_io_tracing' ${testflags} -run ${TESTS} ./objstorage/objstorageprovider/objiotracing + .PHONY: lint lint: ${GO} test -tags '$(TAGS)' ${testflags} -run ${TESTS} ./internal/lint @@ -51,20 +63,31 @@ stressmeta: stress .PHONY: crossversion-meta crossversion-meta: - git checkout ${LATEST_RELEASE}; \ - ${GO} test -c ./internal/metamorphic -o './internal/metamorphic/crossversion/${LATEST_RELEASE}.test'; \ + git checkout ${PREV_RELEASE}; \ + ${GO} test -c ./internal/metamorphic -o './internal/metamorphic/crossversion/${PREV_RELEASE}.test'; \ git checkout -; \ ${GO} test -c ./internal/metamorphic -o './internal/metamorphic/crossversion/head.test'; \ - ${GO} test -tags '$(TAGS)' ${testflags} -v -run 'TestMetaCrossVersion' ./internal/metamorphic/crossversion --version '${LATEST_RELEASE},${LATEST_RELEASE},${LATEST_RELEASE}.test' --version 'HEAD,HEAD,./head.test' + ${GO} test -tags '$(TAGS)' ${testflags} -v -run 'TestMetaCrossVersion' ./internal/metamorphic/crossversion --version '${PREV_RELEASE},${PREV_RELEASE},${PREV_RELEASE}.test' --version 'HEAD,HEAD,./head.test' .PHONY: stress-crossversion stress-crossversion: - STRESS=1 ./scripts/run-crossversion-meta.sh crl-release-21.2 crl-release-22.1 crl-release-22.2 master + STRESS=1 ./scripts/run-crossversion-meta.sh crl-release-21.2 crl-release-22.1 crl-release-22.2 crl-release-23.1 master .PHONY: generate generate: ${GO} generate ${PKG} +generate: + +# Note that the output of generate-test-data is not deterministic. This should +# only be run manually as needed. +.PHONY: generate-test-data +generate-test-data: + ${GO} run -tags make_incorrect_manifests ./tool/make_incorrect_manifests.go + ${GO} run -tags make_test_find_db ./tool/make_test_find_db.go + ${GO} run -tags make_test_sstables ./tool/make_test_sstables.go + ${GO} run -tags make_test_remotecat ./tool/make_test_remotecat.go + mod-update: ${GO} get -u ${GO} mod tidy @@ -92,9 +115,11 @@ endif @${GO} mod tidy $(MAKE) git-clean-check +# TODO(radu): switch back to @latest once bogus doc changes are +# addressed; see https://github.com/cockroachdb/crlfmt/pull/44 .PHONY: format format: - go install github.com/cockroachdb/crlfmt@latest && crlfmt -w -tab 2 . + go install github.com/cockroachdb/crlfmt@44a36ec7 && crlfmt -w -tab 2 . .PHONY: format-check format-check: diff --git a/vendor/github.com/cockroachdb/pebble/README.md b/vendor/github.com/cockroachdb/pebble/README.md index 12ccdb2..e1aa479 100644 --- a/vendor/github.com/cockroachdb/pebble/README.md +++ b/vendor/github.com/cockroachdb/pebble/README.md @@ -1,4 +1,4 @@ -# Pebble [![Build Status](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml) [![GoDoc](https://godoc.org/github.com/cockroachdb/pebble?status.svg)](https://godoc.org/github.com/cockroachdb/pebble) +# Pebble [![Build Status](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml/badge.svg?branch=master)](https://github.com/cockroachdb/pebble/actions/workflows/ci.yaml) [![GoDoc](https://godoc.org/github.com/cockroachdb/pebble?status.svg)](https://godoc.org/github.com/cockroachdb/pebble) [Coverage](https://storage.googleapis.com/crl-codecover-public/pebble/index.html) #### [Nightly benchmarks](https://cockroachdb.github.io/pebble/) @@ -8,9 +8,8 @@ RocksDB file formats and a few extensions such as range deletion tombstones, table-level bloom filters, and updates to the MANIFEST format. -Pebble intentionally does not aspire to include every feature in -RocksDB and is specifically targetting the use case and feature set -needed by CockroachDB: +Pebble intentionally does not aspire to include every feature in RocksDB and +specifically targets the use case and feature set needed by CockroachDB: * Block-based tables * Checkpoints @@ -68,6 +67,17 @@ Pebble offers several improvements over RocksDB: * Faster commit pipeline that achieves better concurrency. * Seamless merged iteration of indexed batches. The mutations in the batch conceptually occupy another memtable level. +* L0 sublevels and flush splitting for concurrent compactions out of L0 and + reduced read-amplification during heavy write load. +* Faster LSM edits in LSMs with large numbers of sstables through use of a + copy-on-write B-tree to hold file metadata. +* Delete-only compactions that drop whole sstables that fall within the bounds + of a range deletion. +* Block-property collectors and filters that enable iterators to skip tables, + index blocks and data blocks that are irrelevant, according to user-defined + properties over key-value pairs. +* Range keys API, allowing KV pairs defined over a range of keyspace with + user-defined semantics and interleaved during iteration. * Smaller, more approachable code base. See the [Pebble vs RocksDB: Implementation @@ -100,10 +110,68 @@ known incompatibilities. format. * Plain table format. Pebble does not support the plain table sstable format. -* SSTable format version 3 and 4. Pebble does not currently support - version 3 and version 4 format sstables. The sstable format version - is controlled by the `BlockBasedTableOptions::format_version` - option. See [#97](https://github.com/cockroachdb/pebble/issues/97). +* SSTable format version 3 and 4. Pebble does not support version 3 + and version 4 format sstables. The sstable format version is + controlled by the `BlockBasedTableOptions::format_version` option. + See [#97](https://github.com/cockroachdb/pebble/issues/97). + +## Format major versions + +Over time Pebble has introduced new physical file formats. Backwards +incompatible changes are made through the introduction of 'format major +versions'. By default, when Pebble opens a database, it defaults to +`FormatMostCompatible`. This version is bi-directionally compatible with RocksDB +6.2.1 (with the caveats described above). + +To opt into new formats, a user may set `FormatMajorVersion` on the +[`Options`](https://pkg.go.dev/github.com/cockroachdb/pebble#Options) +supplied to +[`Open`](https://pkg.go.dev/github.com/cockroachdb/pebble#Open), or +upgrade the format major version at runtime using +[`DB.RatchetFormatMajorVersion`](https://pkg.go.dev/github.com/cockroachdb/pebble#DB.RatchetFormatMajorVersion). +Format major version upgrades are permanent; There is no option to +return to an earlier format. + +The table below outlines the history of format major versions: + +| Name | Value | Migration | +|------------------------------------|-------|------------| +| FormatMostCompatible | 1 | No | +| FormatVersioned | 3 | No | +| FormatSetWithDelete | 4 | No | +| FormatBlockPropertyCollector | 5 | No | +| FormatSplitUserKeysMarked | 6 | Background | +| FormatSplitUserKeysMarkedCompacted | 7 | Blocking | +| FormatRangeKeys | 8 | No | +| FormatMinTableFormatPebblev1 | 9 | No | +| FormatPrePebblev1Marked | 10 | Background | +| FormatSSTableValueBlocks | 12 | No | +| FormatFlushableIngest | 13 | No | +| FormatPrePebblev1MarkedCompacted | 14 | Blocking | +| FormatDeleteSizedAndObsolete | 15 | No | +| FormatVirtualSSTables | 16 | No | + +Upgrading to a format major version with 'Background' in the migration +column may trigger background activity to rewrite physical file +formats, typically through compactions. Upgrading to a format major +version with 'Blocking' in the migration column will block until a +migration is complete. The database may continue to serve reads and +writes if upgrading a live database through +`RatchetFormatMajorVersion`, but the method call will not return until +the migration is complete. + +For reference, the table below lists the range of supported Pebble format major +versions for CockroachDB releases. + +| CockroachDB release | Earliest supported | Latest supported | +|---------------------|------------------------------------|---------------------------| +| 20.1 through 21.1 | FormatMostCompatible | FormatMostCompatible | +| 21.2 | FormatMostCompatible | FormatSetWithDelete | +| 21.2 | FormatMostCompatible | FormatSetWithDelete | +| 22.1 | FormatMostCompatible | FormatSplitUserKeysMarked | +| 22.2 | FormatMostCompatible | FormatPrePebblev1Marked | +| 23.1 | FormatSplitUserKeysMarkedCompacted | FormatFlushableIngest | +| 23.2 | FormatSplitUserKeysMarkedCompacted | FormatVirtualSSTables | ## Pedigree diff --git a/vendor/github.com/cockroachdb/pebble/batch.go b/vendor/github.com/cockroachdb/pebble/batch.go index 263894b..95c7e30 100644 --- a/vendor/github.com/cockroachdb/pebble/batch.go +++ b/vendor/github.com/cockroachdb/pebble/batch.go @@ -41,10 +41,10 @@ const ( var ErrNotIndexed = errors.New("pebble: batch not indexed") // ErrInvalidBatch indicates that a batch is invalid or otherwise corrupted. -var ErrInvalidBatch = errors.New("pebble: invalid batch") +var ErrInvalidBatch = base.MarkCorruptionError(errors.New("pebble: invalid batch")) // ErrBatchTooLarge indicates that a batch is invalid or otherwise corrupted. -var ErrBatchTooLarge = errors.Newf("pebble: batch too large: >= %s", humanize.Uint64(maxBatchSize)) +var ErrBatchTooLarge = base.MarkCorruptionError(errors.Newf("pebble: batch too large: >= %s", humanize.Bytes.Uint64(maxBatchSize))) // DeferredBatchOp represents a batch operation (eg. set, merge, delete) that is // being inserted into the batch. Indexing is not performed on the specified key @@ -183,6 +183,13 @@ func (d DeferredBatchOp) Finish() error { // WAL, and thus stable. New record kinds may be added, but the existing ones // will not be modified. type Batch struct { + batchInternal + applied atomic.Bool +} + +// batchInternal contains the set of fields within Batch that are non-atomic and +// capable of being reset using a *b = batchInternal{} struct copy. +type batchInternal struct { // Data is the wire format of a batch's log entry: // - 8 bytes for a sequence number of the first batch element, // or zeroes if the batch has not yet been applied, @@ -260,10 +267,12 @@ type Batch struct { // memtable. flushable *flushableBatch - // ingestedSSTBatch indicates that the batch contains one or more key kinds - // of InternalKeyKindIngestSST. If the batch contains key kinds of IngestSST - // then it will only contain key kinds of IngestSST. - ingestedSSTBatch bool + // minimumFormatMajorVersion indicates the format major version required in + // order to commit this batch. If an operation requires a particular format + // major version, it ratchets the batch's minimumFormatMajorVersion. When + // the batch is committed, this is validated against the database's current + // format major version. + minimumFormatMajorVersion FormatMajorVersion // Synchronous Apply uses the commit WaitGroup for both publishing the // seqnum and waiting for the WAL fsync (if needed). Asynchronous @@ -285,7 +294,22 @@ type Batch struct { commitStats BatchCommitStats commitErr error - applied uint32 // updated atomically + + // Position bools together to reduce the sizeof the struct. + + // ingestedSSTBatch indicates that the batch contains one or more key kinds + // of InternalKeyKindIngestSST. If the batch contains key kinds of IngestSST + // then it will only contain key kinds of IngestSST. + ingestedSSTBatch bool + + // committing is set to true when a batch begins to commit. It's used to + // ensure the batch is not mutated concurrently. It is not an atomic + // deliberately, so as to avoid the overhead on batch mutations. This is + // okay, because under correct usage this field will never be accessed + // concurrently. It's only under incorrect usage the memory accesses of this + // variable may violate memory safety. Since we don't use atomics here, + // false negatives are possible. + committing bool } // BatchCommitStats exposes stats related to committing a batch. @@ -312,7 +336,11 @@ type BatchCommitStats struct { // commitPipeline.Commit. SemaphoreWaitDuration time.Duration // WALQueueWaitDuration is the wait time for allocating memory blocks in the - // LogWriter (due to the LogWriter not writing fast enough). + // LogWriter (due to the LogWriter not writing fast enough). At the moment + // this is duration is always zero because a single WAL will allow + // allocating memory blocks up to the entire memtable size. In the future, + // we may pipeline WALs and bound the WAL queued blocks separately, so this + // field is preserved for that possibility. WALQueueWaitDuration time.Duration // MemTableWriteStallDuration is the wait caused by a write stall due to too // many memtables (due to not flushing fast enough). @@ -356,6 +384,14 @@ func newBatch(db *DB) *Batch { return b } +func newBatchWithSize(db *DB, size int) *Batch { + b := newBatch(db) + if cap(b.data) < size { + b.data = rawalloc.New(0, size) + } + return b +} + func newIndexedBatch(db *DB, comparer *Comparer) *Batch { i := indexedBatchPool.Get().(*indexedBatch) i.batch.cmp = comparer.Compare @@ -367,6 +403,14 @@ func newIndexedBatch(db *DB, comparer *Comparer) *Batch { return &i.batch } +func newIndexedBatchWithSize(db *DB, comparer *Comparer, size int) *Batch { + b := newIndexedBatch(db, comparer) + if cap(b.data) < size { + b.data = rawalloc.New(0, size) + } + return b +} + // nextSeqNum returns the batch "sequence number" that will be given to the next // key written to the batch. During iteration keys within an indexed batch are // given a sequence number consisting of their offset within the batch combined @@ -405,17 +449,21 @@ func (b *Batch) release() { } } -func (b *Batch) refreshMemTableSize() { +func (b *Batch) refreshMemTableSize() error { b.memTableSize = 0 if len(b.data) < batchHeaderLen { - return + return nil } b.countRangeDels = 0 b.countRangeKeys = 0 + b.minimumFormatMajorVersion = 0 for r := b.Reader(); ; { - kind, key, value, ok := r.Next() + kind, key, value, ok, err := r.Next() if !ok { + if err != nil { + return err + } break } switch kind { @@ -423,12 +471,23 @@ func (b *Batch) refreshMemTableSize() { b.countRangeDels++ case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: b.countRangeKeys++ + case InternalKeyKindDeleteSized: + if b.minimumFormatMajorVersion < FormatDeleteSizedAndObsolete { + b.minimumFormatMajorVersion = FormatDeleteSizedAndObsolete + } case InternalKeyKindIngestSST: + if b.minimumFormatMajorVersion < FormatFlushableIngest { + b.minimumFormatMajorVersion = FormatFlushableIngest + } // This key kind doesn't contribute to the memtable size. continue } b.memTableSize += memTableEntrySize(len(key), len(value)) } + if b.countRangeKeys > 0 && b.minimumFormatMajorVersion < FormatRangeKeys { + b.minimumFormatMajorVersion = FormatRangeKeys + } + return nil } // Apply the operations contained in the batch to the receiver batch. @@ -442,7 +501,7 @@ func (b *Batch) Apply(batch *Batch, _ *WriteOptions) error { return nil } if len(batch.data) < batchHeaderLen { - return base.CorruptionErrorf("pebble: invalid batch") + return ErrInvalidBatch } offset := len(b.data) @@ -459,8 +518,11 @@ func (b *Batch) Apply(batch *Batch, _ *WriteOptions) error { // order to update the index. for iter := BatchReader(b.data[offset:]); len(iter) > 0; { offset := uintptr(unsafe.Pointer(&iter[0])) - uintptr(unsafe.Pointer(&b.data[0])) - kind, key, value, ok := iter.Next() + kind, key, value, ok, err := iter.Next() if !ok { + if err != nil { + return err + } break } switch kind { @@ -516,6 +578,9 @@ func (b *Batch) Get(key []byte) ([]byte, io.Closer, error) { } func (b *Batch) prepareDeferredKeyValueRecord(keyLen, valueLen int, kind InternalKeyKind) { + if b.committing { + panic("pebble: batch already committing") + } if len(b.data) == 0 { b.init(keyLen + valueLen + 2*binary.MaxVarintLen64 + batchHeaderLen) } @@ -565,6 +630,9 @@ func (b *Batch) prepareDeferredKeyValueRecord(keyLen, valueLen int, kind Interna } func (b *Batch) prepareDeferredKeyRecord(keyLen int, kind InternalKeyKind) { + if b.committing { + panic("pebble: batch already committing") + } if len(b.data) == 0 { b.init(keyLen + binary.MaxVarintLen64 + batchHeaderLen) } @@ -598,6 +666,41 @@ func (b *Batch) prepareDeferredKeyRecord(keyLen int, kind InternalKeyKind) { b.data = b.data[:pos+keyLen] } +// AddInternalKey allows the caller to add an internal key of point key kinds to +// a batch. Passing in an internal key of kind RangeKey* or RangeDelete will +// result in a panic. Note that the seqnum in the internal key is effectively +// ignored, even though the Kind is preserved. This is because the batch format +// does not allow for a per-key seqnum to be specified, only a batch-wide one. +// +// Note that non-indexed keys (IngestKeyKind{LogData,IngestSST}) are not +// supported with this method as they require specialized logic. +func (b *Batch) AddInternalKey(key *base.InternalKey, value []byte, _ *WriteOptions) error { + keyLen := len(key.UserKey) + hasValue := false + switch key.Kind() { + case InternalKeyKindRangeDelete, InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + panic("unexpected range delete or range key kind in AddInternalKey") + case InternalKeyKindSingleDelete, InternalKeyKindDelete: + b.prepareDeferredKeyRecord(len(key.UserKey), key.Kind()) + default: + b.prepareDeferredKeyValueRecord(keyLen, len(value), key.Kind()) + hasValue = true + } + b.deferredOp.index = b.index + copy(b.deferredOp.Key, key.UserKey) + if hasValue { + copy(b.deferredOp.Value, value) + } + // TODO(peter): Manually inline DeferredBatchOp.Finish(). Mid-stack inlining + // in go1.13 will remove the need for this. + if b.index != nil { + if err := b.index.Add(b.deferredOp.offset); err != nil { + return err + } + } + return nil +} + // Set adds an action to the batch that sets the key to map to the value. // // It is safe to modify the contents of the arguments after Set returns. @@ -680,6 +783,72 @@ func (b *Batch) DeleteDeferred(keyLen int) *DeferredBatchOp { return &b.deferredOp } +// DeleteSized behaves identically to Delete, but takes an additional +// argument indicating the size of the value being deleted. DeleteSized +// should be preferred when the caller has the expectation that there exists +// a single internal KV pair for the key (eg, the key has not been +// overwritten recently), and the caller knows the size of its value. +// +// DeleteSized will record the value size within the tombstone and use it to +// inform compaction-picking heuristics which strive to reduce space +// amplification in the LSM. This "calling your shot" mechanic allows the +// storage engine to more accurately estimate and reduce space amplification. +// +// It is safe to modify the contents of the arguments after DeleteSized +// returns. +func (b *Batch) DeleteSized(key []byte, deletedValueSize uint32, _ *WriteOptions) error { + deferredOp := b.DeleteSizedDeferred(len(key), deletedValueSize) + copy(b.deferredOp.Key, key) + // TODO(peter): Manually inline DeferredBatchOp.Finish(). Check if in a + // later Go release this is unnecessary. + if b.index != nil { + if err := b.index.Add(deferredOp.offset); err != nil { + return err + } + } + return nil +} + +// DeleteSizedDeferred is similar to DeleteSized in that it adds a sized delete +// operation to the batch, except it only takes in key length instead of a +// complete key slice, letting the caller encode into the DeferredBatchOp.Key +// slice and then call Finish() on the returned object. +func (b *Batch) DeleteSizedDeferred(keyLen int, deletedValueSize uint32) *DeferredBatchOp { + if b.minimumFormatMajorVersion < FormatDeleteSizedAndObsolete { + b.minimumFormatMajorVersion = FormatDeleteSizedAndObsolete + } + + // Encode the sum of the key length and the value in the value. + v := uint64(deletedValueSize) + uint64(keyLen) + + // Encode `v` as a varint. + var buf [binary.MaxVarintLen64]byte + n := 0 + { + x := v + for x >= 0x80 { + buf[n] = byte(x) | 0x80 + x >>= 7 + n++ + } + buf[n] = byte(x) + n++ + } + + // NB: In batch entries and sstable entries, values are stored as + // varstrings. Here, the value is itself a simple varint. This results in an + // unnecessary double layer of encoding: + // varint(n) varint(deletedValueSize) + // The first varint will always be 1-byte, since a varint-encoded uint64 + // will never exceed 128 bytes. This unnecessary extra byte and wrapping is + // preserved to avoid special casing across the database, and in particular + // in sstable block decoding which is performance sensitive. + b.prepareDeferredKeyValueRecord(keyLen, n, InternalKeyKindDeleteSized) + b.deferredOp.index = b.index + copy(b.deferredOp.Value, buf[:n]) + return &b.deferredOp +} + // SingleDelete adds an action to the batch that single deletes the entry for key. // See Writer.SingleDelete for more details on the semantics of SingleDelete. // @@ -782,6 +951,9 @@ func (b *Batch) rangeKeySetDeferred(startLen, internalValueLen int) *DeferredBat func (b *Batch) incrementRangeKeysCount() { b.countRangeKeys++ + if b.minimumFormatMajorVersion < FormatRangeKeys { + b.minimumFormatMajorVersion = FormatRangeKeys + } if b.index != nil { b.rangeKeys = nil b.rangeKeysSeqNum = 0 @@ -895,6 +1067,7 @@ func (b *Batch) ingestSST(fileNum base.FileNum) { // is not reset because for the InternalKeyKindIngestSST the count is the // number of sstable paths which have been added to the batch. b.memTableSize = origMemTableSize + b.minimumFormatMajorVersion = FormatFlushableIngest } // Empty returns true if the batch is empty, and false otherwise. @@ -930,11 +1103,12 @@ func (b *Batch) SetRepr(data []byte) error { } b.data = data b.count = uint64(binary.LittleEndian.Uint32(b.countData())) + var err error if b.db != nil { // Only track memTableSize for batches that will be committed to the DB. - b.refreshMemTableSize() + err = b.refreshMemTableSize() } - return nil + return err } // NewIter returns an iterator that is unpositioned (Iterator.Valid() will @@ -944,8 +1118,8 @@ func (b *Batch) SetRepr(data []byte) error { // The returned Iterator observes all of the Batch's existing mutations, but no // later mutations. Its view can be refreshed via RefreshBatchSnapshot or // SetOptions(). -func (b *Batch) NewIter(o *IterOptions) *Iterator { - return b.NewIterWithContext(context.Background(), o) +func (b *Batch) NewIter(o *IterOptions) (*Iterator, error) { + return b.NewIterWithContext(context.Background(), o), nil } // NewIterWithContext is like NewIter, and additionally accepts a context for @@ -954,7 +1128,7 @@ func (b *Batch) NewIterWithContext(ctx context.Context, o *IterOptions) *Iterato if b.index == nil { return &Iterator{err: ErrNotIndexed} } - return b.db.newIter(ctx, b, nil /* snapshot */, o) + return b.db.newIter(ctx, b, snapshotIterOpts{}, o) } // newInternalIter creates a new internalIterator that iterates over the @@ -1167,38 +1341,40 @@ func (b *Batch) Indexed() bool { return b.index != nil } -func (b *Batch) init(cap int) { +// init ensures that the batch data slice is initialized to meet the +// minimum required size and allocates space for the batch header. +func (b *Batch) init(size int) { n := batchInitialSize - for n < cap { + for n < size { n *= 2 } - b.data = rawalloc.New(batchHeaderLen, n) - b.setCount(0) - b.setSeqNum(0) + if cap(b.data) < n { + b.data = rawalloc.New(batchHeaderLen, n) + } b.data = b.data[:batchHeaderLen] + // Zero the sequence number in the header. + for i := 0; i < len(b.data); i++ { + b.data[i] = 0 + } } // Reset resets the batch for reuse. The underlying byte slice (that is -// returned by Repr()) is not modified. It is only necessary to call this +// returned by Repr()) may not be modified. It is only necessary to call this // method if a batch is explicitly being reused. Close automatically takes are // of releasing resources when appropriate for batches that are internally // being reused. func (b *Batch) Reset() { - b.count = 0 - b.countRangeDels = 0 - b.countRangeKeys = 0 - b.memTableSize = 0 - b.deferredOp = DeferredBatchOp{} - b.tombstones = nil - b.tombstonesSeqNum = 0 - b.rangeKeys = nil - b.rangeKeysSeqNum = 0 - b.flushable = nil - b.commit = sync.WaitGroup{} - b.fsyncWait = sync.WaitGroup{} - b.commitStats = BatchCommitStats{} - b.commitErr = nil - atomic.StoreUint32(&b.applied, 0) + // Zero out the struct, retaining only the fields necessary for manual + // reuse. + b.batchInternal = batchInternal{ + data: b.data, + cmp: b.cmp, + formatKey: b.formatKey, + abbreviatedKey: b.abbreviatedKey, + index: b.index, + db: b.db, + } + b.applied.Store(false) if b.data != nil { if cap(b.data) > batchMaxRetainedSize { // If the capacity of the buffer is larger than our maximum @@ -1209,13 +1385,14 @@ func (b *Batch) Reset() { } else { // Otherwise, reset the buffer for re-use. b.data = b.data[:batchHeaderLen] - b.setSeqNum(0) + // Zero the sequence number in the header. + for i := 0; i < len(b.data); i++ { + b.data[i] = 0 + } } } if b.index != nil { b.index.Init(&b.data, b.cmp, b.abbreviatedKey) - b.rangeDelIndex = nil - b.rangeKeyIndex = nil } } @@ -1287,6 +1464,11 @@ func (b *Batch) Reader() BatchReader { } func batchDecodeStr(data []byte) (odata []byte, s []byte, ok bool) { + // TODO(jackson): This will index out of bounds if there's no varint or an + // invalid varint (eg, a single 0xff byte). Correcting will add a bit of + // overhead. We could avoid that overhead whenever len(data) >= + // binary.MaxVarint32? + var v uint32 var n int ptr := unsafe.Pointer(&data[0]) @@ -1349,29 +1531,32 @@ func ReadBatch(repr []byte) (r BatchReader, count uint32) { return repr[batchHeaderLen:], count } -// Next returns the next entry in this batch. The final return value is false -// if the batch is corrupt. The end of batch is reached when len(r)==0. -func (r *BatchReader) Next() (kind InternalKeyKind, ukey []byte, value []byte, ok bool) { +// Next returns the next entry in this batch, if there is one. If the reader has +// reached the end of the batch, Next returns ok=false and a nil error. If the +// batch is corrupt and the next entry is illegible, Next returns ok=false and a +// non-nil error. +func (r *BatchReader) Next() (kind InternalKeyKind, ukey []byte, value []byte, ok bool, err error) { if len(*r) == 0 { - return 0, nil, nil, false + return 0, nil, nil, false, nil } kind = InternalKeyKind((*r)[0]) if kind > InternalKeyKindMax { - return 0, nil, nil, false + return 0, nil, nil, false, errors.Wrapf(ErrInvalidBatch, "invalid key kind 0x%x", (*r)[0]) } *r, ukey, ok = batchDecodeStr((*r)[1:]) if !ok { - return 0, nil, nil, false + return 0, nil, nil, false, errors.Wrapf(ErrInvalidBatch, "decoding user key") } switch kind { case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, - InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete, + InternalKeyKindDeleteSized: *r, value, ok = batchDecodeStr(*r) if !ok { - return 0, nil, nil, false + return 0, nil, nil, false, errors.Wrapf(ErrInvalidBatch, "decoding %s value", kind) } } - return kind, ukey, value, true + return kind, ukey, value, true, nil } // Note: batchIter mirrors the implementation of flushableBatchIter. Keep the @@ -1500,7 +1685,8 @@ func (i *batchIter) value() []byte { switch InternalKeyKind(data[offset]) { case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, - InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete, + InternalKeyKindDeleteSized: _, value, ok := batchDecodeStr(data[keyEnd:]) if !ok { return nil @@ -1572,7 +1758,7 @@ var _ flushable = (*flushableBatch)(nil) // interface. This allows the batch to act like a memtable and be placed in the // queue of flushable memtables. Note that the flushable batch takes ownership // of the batch data. -func newFlushableBatch(batch *Batch, comparer *Comparer) *flushableBatch { +func newFlushableBatch(batch *Batch, comparer *Comparer) (*flushableBatch, error) { b := &flushableBatch{ data: batch.data, cmp: comparer.Compare, @@ -1593,8 +1779,11 @@ func newFlushableBatch(batch *Batch, comparer *Comparer) *flushableBatch { var index uint32 for iter := BatchReader(b.data[batchHeaderLen:]); len(iter) > 0; index++ { offset := uintptr(unsafe.Pointer(&iter[0])) - uintptr(unsafe.Pointer(&b.data[0])) - kind, key, _, ok := iter.Next() + kind, key, _, ok, err := iter.Next() if !ok { + if err != nil { + return nil, err + } break } entry := flushableBatchEntry{ @@ -1666,7 +1855,7 @@ func newFlushableBatch(batch *Batch, comparer *Comparer) *flushableBatch { } fragmentRangeKeys(frag, it, len(rangeKeyOffsets)) } - return b + return b, nil } func (b *flushableBatch) setSeqNum(seqNum uint64) { @@ -1715,6 +1904,7 @@ func (b *flushableBatch) Swap(i, j int) { b.offsets[i], b.offsets[j] = b.offsets[j], b.offsets[i] } +// newIter is part of the flushable interface. func (b *flushableBatch) newIter(o *IterOptions) internalIterator { return &flushableBatchIter{ batch: b, @@ -1727,6 +1917,7 @@ func (b *flushableBatch) newIter(o *IterOptions) internalIterator { } } +// newFlushIter is part of the flushable interface. func (b *flushableBatch) newFlushIter(o *IterOptions, bytesFlushed *uint64) internalIterator { return &flushFlushableBatchIter{ flushableBatchIter: flushableBatchIter{ @@ -1740,6 +1931,7 @@ func (b *flushableBatch) newFlushIter(o *IterOptions, bytesFlushed *uint64) inte } } +// newRangeDelIter is part of the flushable interface. func (b *flushableBatch) newRangeDelIter(o *IterOptions) keyspan.FragmentIterator { if len(b.tombstones) == 0 { return nil @@ -1747,6 +1939,7 @@ func (b *flushableBatch) newRangeDelIter(o *IterOptions) keyspan.FragmentIterato return keyspan.NewIter(b.cmp, b.tombstones) } +// newRangeKeyIter is part of the flushable interface. func (b *flushableBatch) newRangeKeyIter(o *IterOptions) keyspan.FragmentIterator { if len(b.rangeKeys) == 0 { return nil @@ -1754,17 +1947,23 @@ func (b *flushableBatch) newRangeKeyIter(o *IterOptions) keyspan.FragmentIterato return keyspan.NewIter(b.cmp, b.rangeKeys) } +// containsRangeKeys is part of the flushable interface. func (b *flushableBatch) containsRangeKeys() bool { return len(b.rangeKeys) > 0 } +// inuseBytes is part of the flushable interface. func (b *flushableBatch) inuseBytes() uint64 { return uint64(len(b.data) - batchHeaderLen) } +// totalBytes is part of the flushable interface. func (b *flushableBatch) totalBytes() uint64 { return uint64(cap(b.data)) } +// readyForFlush is part of the flushable interface. func (b *flushableBatch) readyForFlush() bool { + // A flushable batch is always ready for flush; it must be flushed together + // with the previous memtable. return true } @@ -1944,7 +2143,8 @@ func (i *flushableBatchIter) value() base.LazyValue { var ok bool switch kind { case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindRangeDelete, - InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete, + InternalKeyKindDeleteSized: keyEnd := i.offsets[i.index].keyEnd _, value, ok = batchDecodeStr(i.data[keyEnd:]) if !ok { @@ -2081,7 +2281,10 @@ func batchSort( rangeKeyIter := b.newRangeKeyIter(nil, math.MaxUint64) return pointIter, rangeDelIter, rangeKeyIter } - f := newFlushableBatch(b, b.db.opts.Comparer) + f, err := newFlushableBatch(b, b.db.opts.Comparer) + if err != nil { + panic(err) + } return f.newIter(nil), f.newRangeDelIter(nil), f.newRangeKeyIter(nil) } diff --git a/vendor/github.com/cockroachdb/pebble/checkpoint.go b/vendor/github.com/cockroachdb/pebble/checkpoint.go index cf019ad..ad70a35 100644 --- a/vendor/github.com/cockroachdb/pebble/checkpoint.go +++ b/vendor/github.com/cockroachdb/pebble/checkpoint.go @@ -188,7 +188,10 @@ func (d *DB) Checkpoint( manifestFileNum := d.mu.versions.manifestFileNum manifestSize := d.mu.versions.manifest.Size() optionsFileNum := d.optionsFileNum - + virtualBackingFiles := make(map[base.DiskFileNum]struct{}) + for diskFileNum := range d.mu.versions.backingState.fileBackingMap { + virtualBackingFiles[diskFileNum] = struct{}{} + } // Release the manifest and DB.mu so we don't block other operations on // the database. d.mu.versions.logUnlock() @@ -209,11 +212,7 @@ func (d *DB) Checkpoint( } if ckErr != nil { // Attempt to cleanup on error. - paths, _ := fs.List(destDir) - for _, path := range paths { - _ = fs.Remove(path) - } - _ = fs.Remove(destDir) + _ = fs.RemoveAll(destDir) } }() dir, ckErr = mkdirAllAndSyncParents(fs, destDir) @@ -254,7 +253,9 @@ func (d *DB) Checkpoint( } var excludedFiles map[deletedFileEntry]*fileMetadata - + // Set of FileBacking.DiskFileNum which will be required by virtual sstables + // in the checkpoint. + requiredVirtualBackingFiles := make(map[base.DiskFileNum]struct{}) // Link or copy the sstables. for l := range current.Levels { iter := current.Levels[l].Iter() @@ -270,7 +271,15 @@ func (d *DB) Checkpoint( continue } - srcPath := base.MakeFilepath(fs, d.dirname, fileTypeTable, f.FileNum) + fileBacking := f.FileBacking + if f.Virtual { + if _, ok := requiredVirtualBackingFiles[fileBacking.DiskFileNum]; ok { + continue + } + requiredVirtualBackingFiles[fileBacking.DiskFileNum] = struct{}{} + } + + srcPath := base.MakeFilepath(fs, d.dirname, fileTypeTable, fileBacking.DiskFileNum) destPath := fs.PathJoin(destDir, fs.PathBase(srcPath)) ckErr = vfs.LinkOrCopy(fs, srcPath, destPath) if ckErr != nil { @@ -279,7 +288,19 @@ func (d *DB) Checkpoint( } } - ckErr = d.writeCheckpointManifest(fs, formatVers, destDir, dir, manifestFileNum, manifestSize, excludedFiles) + var removeBackingTables []base.DiskFileNum + for diskFileNum := range virtualBackingFiles { + if _, ok := requiredVirtualBackingFiles[diskFileNum]; !ok { + // The backing sstable associated with fileNum is no longer + // required. + removeBackingTables = append(removeBackingTables, diskFileNum) + } + } + + ckErr = d.writeCheckpointManifest( + fs, formatVers, destDir, dir, manifestFileNum.DiskFileNum(), manifestSize, + excludedFiles, removeBackingTables, + ) if ckErr != nil { return ckErr } @@ -292,7 +313,7 @@ func (d *DB) Checkpoint( if logNum == 0 { continue } - srcPath := base.MakeFilepath(fs, d.walDirname, fileTypeLog, logNum) + srcPath := base.MakeFilepath(fs, d.walDirname, fileTypeLog, logNum.DiskFileNum()) destPath := fs.PathJoin(destDir, fs.PathBase(srcPath)) ckErr = vfs.Copy(fs, srcPath, destPath) if ckErr != nil { @@ -315,9 +336,10 @@ func (d *DB) writeCheckpointManifest( formatVers FormatMajorVersion, destDirPath string, destDir vfs.File, - manifestFileNum FileNum, + manifestFileNum base.DiskFileNum, manifestSize int64, excludedFiles map[deletedFileEntry]*fileMetadata, + removeBackingTables []base.DiskFileNum, ) error { // Copy the MANIFEST, and create a pointer to it. We copy rather // than link because additional version edits added to the @@ -346,7 +368,7 @@ func (d *DB) writeCheckpointManifest( // need to append another record with the excluded files (we cannot simply // append a record after a raw data copy; see // https://github.com/cockroachdb/cockroach/issues/100935). - r := record.NewReader(&io.LimitedReader{R: src, N: manifestSize}, manifestFileNum) + r := record.NewReader(&io.LimitedReader{R: src, N: manifestSize}, manifestFileNum.FileNum()) w := record.NewWriter(dst) for { rr, err := r.Next() @@ -369,8 +391,10 @@ func (d *DB) writeCheckpointManifest( if len(excludedFiles) > 0 { // Write out an additional VersionEdit that deletes the excluded SST files. ve := versionEdit{ - DeletedFiles: excludedFiles, + DeletedFiles: excludedFiles, + RemovedBackingTables: removeBackingTables, } + rw, err := w.Next() if err != nil { return err @@ -397,7 +421,7 @@ func (d *DB) writeCheckpointManifest( if err != nil { return err } - if err := setCurrentFunc(formatVers, manifestMarker, fs, destDirPath, destDir)(manifestFileNum); err != nil { + if err := setCurrentFunc(formatVers, manifestMarker, fs, destDirPath, destDir)(manifestFileNum.FileNum()); err != nil { return err } return manifestMarker.Close() diff --git a/vendor/github.com/cockroachdb/pebble/cleaner.go b/vendor/github.com/cockroachdb/pebble/cleaner.go index 384bff5..f9fa43b 100644 --- a/vendor/github.com/cockroachdb/pebble/cleaner.go +++ b/vendor/github.com/cockroachdb/pebble/cleaner.go @@ -4,7 +4,18 @@ package pebble -import "github.com/cockroachdb/pebble/internal/base" +import ( + "context" + "runtime/pprof" + "sync" + "time" + + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/tokenbucket" +) // Cleaner exports the base.Cleaner type. type Cleaner = base.Cleaner @@ -14,3 +25,271 @@ type DeleteCleaner = base.DeleteCleaner // ArchiveCleaner exports the base.ArchiveCleaner type. type ArchiveCleaner = base.ArchiveCleaner + +type cleanupManager struct { + opts *Options + objProvider objstorage.Provider + onTableDeleteFn func(fileSize uint64) + deletePacer *deletionPacer + + // jobsCh is used as the cleanup job queue. + jobsCh chan *cleanupJob + // waitGroup is used to wait for the background goroutine to exit. + waitGroup sync.WaitGroup + + mu struct { + sync.Mutex + // totalJobs is the total number of enqueued jobs (completed or in progress). + totalJobs int + completedJobs int + completedJobsCond sync.Cond + jobsQueueWarningIssued bool + } +} + +// We can queue this many jobs before we have to block EnqueueJob. +const jobsQueueDepth = 1000 + +// obsoleteFile holds information about a file that needs to be deleted soon. +type obsoleteFile struct { + dir string + fileNum base.DiskFileNum + fileType fileType + fileSize uint64 +} + +type cleanupJob struct { + jobID int + obsoleteFiles []obsoleteFile +} + +// openCleanupManager creates a cleanupManager and starts its background goroutine. +// The cleanupManager must be Close()d. +func openCleanupManager( + opts *Options, + objProvider objstorage.Provider, + onTableDeleteFn func(fileSize uint64), + getDeletePacerInfo func() deletionPacerInfo, +) *cleanupManager { + cm := &cleanupManager{ + opts: opts, + objProvider: objProvider, + onTableDeleteFn: onTableDeleteFn, + deletePacer: newDeletionPacer(time.Now(), int64(opts.TargetByteDeletionRate), getDeletePacerInfo), + jobsCh: make(chan *cleanupJob, jobsQueueDepth), + } + cm.mu.completedJobsCond.L = &cm.mu.Mutex + cm.waitGroup.Add(1) + + go func() { + pprof.Do(context.Background(), gcLabels, func(context.Context) { + cm.mainLoop() + }) + }() + + return cm +} + +// Close stops the background goroutine, waiting until all queued jobs are completed. +// Delete pacing is disabled for the remaining jobs. +func (cm *cleanupManager) Close() { + close(cm.jobsCh) + cm.waitGroup.Wait() +} + +// EnqueueJob adds a cleanup job to the manager's queue. +func (cm *cleanupManager) EnqueueJob(jobID int, obsoleteFiles []obsoleteFile) { + job := &cleanupJob{ + jobID: jobID, + obsoleteFiles: obsoleteFiles, + } + + // Report deleted bytes to the pacer, which can use this data to potentially + // increase the deletion rate to keep up. We want to do this at enqueue time + // rather than when we get to the job, otherwise the reported bytes will be + // subject to the throttling rate which defeats the purpose. + var pacingBytes uint64 + for _, of := range obsoleteFiles { + if cm.needsPacing(of.fileType, of.fileNum) { + pacingBytes += of.fileSize + } + } + if pacingBytes > 0 { + cm.deletePacer.ReportDeletion(time.Now(), pacingBytes) + } + + cm.mu.Lock() + cm.mu.totalJobs++ + cm.maybeLogLocked() + cm.mu.Unlock() + + if invariants.Enabled && len(cm.jobsCh) >= cap(cm.jobsCh)-2 { + panic("cleanup jobs queue full") + } + + cm.jobsCh <- job +} + +// Wait until the completion of all jobs that were already queued. +// +// Does not wait for jobs that are enqueued during the call. +// +// Note that DB.mu should not be held while calling this method; the background +// goroutine needs to acquire DB.mu to update deleted table metrics. +func (cm *cleanupManager) Wait() { + cm.mu.Lock() + defer cm.mu.Unlock() + n := cm.mu.totalJobs + for cm.mu.completedJobs < n { + cm.mu.completedJobsCond.Wait() + } +} + +// mainLoop runs the manager's background goroutine. +func (cm *cleanupManager) mainLoop() { + defer cm.waitGroup.Done() + + var tb tokenbucket.TokenBucket + // Use a token bucket with 1 token / second refill rate and 1 token burst. + tb.Init(1.0, 1.0) + for job := range cm.jobsCh { + for _, of := range job.obsoleteFiles { + if of.fileType != fileTypeTable { + path := base.MakeFilepath(cm.opts.FS, of.dir, of.fileType, of.fileNum) + cm.deleteObsoleteFile(of.fileType, job.jobID, path, of.fileNum, of.fileSize) + } else { + cm.maybePace(&tb, of.fileType, of.fileNum, of.fileSize) + cm.onTableDeleteFn(of.fileSize) + cm.deleteObsoleteObject(fileTypeTable, job.jobID, of.fileNum) + } + } + cm.mu.Lock() + cm.mu.completedJobs++ + cm.mu.completedJobsCond.Broadcast() + cm.maybeLogLocked() + cm.mu.Unlock() + } +} + +func (cm *cleanupManager) needsPacing(fileType base.FileType, fileNum base.DiskFileNum) bool { + if fileType != fileTypeTable { + return false + } + meta, err := cm.objProvider.Lookup(fileType, fileNum) + if err != nil { + // The object was already removed from the provider; we won't actually + // delete anything, so we don't need to pace. + return false + } + // Don't throttle deletion of remote objects. + return !meta.IsRemote() +} + +// maybePace sleeps before deleting an object if appropriate. It is always +// called from the background goroutine. +func (cm *cleanupManager) maybePace( + tb *tokenbucket.TokenBucket, fileType base.FileType, fileNum base.DiskFileNum, fileSize uint64, +) { + if !cm.needsPacing(fileType, fileNum) { + return + } + + tokens := cm.deletePacer.PacingDelay(time.Now(), fileSize) + if tokens == 0.0 { + // The token bucket might be in debt; it could make us wait even for 0 + // tokens. We don't want that if the pacer decided throttling should be + // disabled. + return + } + // Wait for tokens. We use a token bucket instead of sleeping outright because + // the token bucket accumulates up to one second of unused tokens. + for { + ok, d := tb.TryToFulfill(tokenbucket.Tokens(tokens)) + if ok { + break + } + time.Sleep(d) + } +} + +// deleteObsoleteFile deletes a (non-object) file that is no longer needed. +func (cm *cleanupManager) deleteObsoleteFile( + fileType fileType, jobID int, path string, fileNum base.DiskFileNum, fileSize uint64, +) { + // TODO(peter): need to handle this error, probably by re-adding the + // file that couldn't be deleted to one of the obsolete slices map. + err := cm.opts.Cleaner.Clean(cm.opts.FS, fileType, path) + if oserror.IsNotExist(err) { + return + } + + switch fileType { + case fileTypeLog: + cm.opts.EventListener.WALDeleted(WALDeleteInfo{ + JobID: jobID, + Path: path, + FileNum: fileNum.FileNum(), + Err: err, + }) + case fileTypeManifest: + cm.opts.EventListener.ManifestDeleted(ManifestDeleteInfo{ + JobID: jobID, + Path: path, + FileNum: fileNum.FileNum(), + Err: err, + }) + case fileTypeTable: + panic("invalid deletion of object file") + } +} + +func (cm *cleanupManager) deleteObsoleteObject( + fileType fileType, jobID int, fileNum base.DiskFileNum, +) { + if fileType != fileTypeTable { + panic("not an object") + } + + var path string + meta, err := cm.objProvider.Lookup(fileType, fileNum) + if err != nil { + path = "" + } else { + path = cm.objProvider.Path(meta) + err = cm.objProvider.Remove(fileType, fileNum) + } + if cm.objProvider.IsNotExistError(err) { + return + } + + switch fileType { + case fileTypeTable: + cm.opts.EventListener.TableDeleted(TableDeleteInfo{ + JobID: jobID, + Path: path, + FileNum: fileNum.FileNum(), + Err: err, + }) + } +} + +// maybeLogLocked issues a log if the job queue gets 75% full and issues a log +// when the job queue gets back to less than 10% full. +// +// Must be called with cm.mu locked. +func (cm *cleanupManager) maybeLogLocked() { + const highThreshold = jobsQueueDepth * 3 / 4 + const lowThreshold = jobsQueueDepth / 10 + + jobsInQueue := cm.mu.totalJobs - cm.mu.completedJobs + + if !cm.mu.jobsQueueWarningIssued && jobsInQueue > highThreshold { + cm.mu.jobsQueueWarningIssued = true + cm.opts.Logger.Infof("cleanup falling behind; job queue has over %d jobs", highThreshold) + } + + if cm.mu.jobsQueueWarningIssued && jobsInQueue < lowThreshold { + cm.mu.jobsQueueWarningIssued = false + cm.opts.Logger.Infof("cleanup back to normal; job queue has under %d jobs", lowThreshold) + } +} diff --git a/vendor/github.com/cockroachdb/pebble/commit.go b/vendor/github.com/cockroachdb/pebble/commit.go index 38ede48..38cdbb8 100644 --- a/vendor/github.com/cockroachdb/pebble/commit.go +++ b/vendor/github.com/cockroachdb/pebble/commit.go @@ -9,7 +9,6 @@ import ( "sync" "sync/atomic" "time" - "unsafe" "github.com/cockroachdb/pebble/record" ) @@ -33,13 +32,13 @@ type commitQueue struct { // // The head index is stored in the most-significant bits so that we can // atomically add to it and the overflow is harmless. - headTail uint64 + headTail atomic.Uint64 // slots is a ring buffer of values stored in this queue. The size must be a // power of 2. A slot is in use until *both* the tail index has moved beyond // it and the slot value has been set to nil. The slot value is set to nil // atomically by the consumer and read atomically by the producer. - slots [record.SyncConcurrency]unsafe.Pointer + slots [record.SyncConcurrency]atomic.Pointer[Batch] } const dequeueBits = 32 @@ -58,7 +57,7 @@ func (q *commitQueue) pack(head, tail uint32) uint64 { } func (q *commitQueue) enqueue(b *Batch) { - ptrs := atomic.LoadUint64(&q.headTail) + ptrs := q.headTail.Load() head, tail := q.unpack(ptrs) if (tail+uint32(len(q.slots)))&(1< 2 { + info.Annotations = append(info.Annotations, "multilevel") + } return info } -func newCompaction(pc *pickedCompaction, opts *Options) *compaction { +func newCompaction( + pc *pickedCompaction, opts *Options, beganAt time.Time, provider objstorage.Provider, +) *compaction { c := &compaction{ kind: compactionKindDefault, cmp: pc.cmp, equal: opts.equal(), comparer: opts.Comparer, formatKey: opts.Comparer.FormatKey, - score: pc.score, inputs: pc.inputs, smallest: pc.smallest, largest: pc.largest, logger: opts.Logger, version: pc.version, + beganAt: beganAt, maxOutputFileSize: pc.maxOutputFileSize, maxOverlapBytes: pc.maxOverlapBytes, - l0SublevelInfo: pc.l0SublevelInfo, + pickerMetrics: pc.pickerMetrics, } c.startLevel = &c.inputs[0] + if pc.startLevel.l0SublevelInfo != nil { + c.startLevel.l0SublevelInfo = pc.startLevel.l0SublevelInfo + } c.outputLevel = &c.inputs[1] if len(pc.extraLevels) > 0 { @@ -714,20 +754,48 @@ func newCompaction(pc *pickedCompaction, opts *Options) *compaction { c.smallest.UserKey, c.largest.UserKey, c.largest.IsExclusiveSentinel()) } c.setupInuseKeyRanges() - c.kind = pc.kind + if c.kind == compactionKindDefault && c.outputLevel.files.Empty() && !c.hasExtraLevelData() && c.startLevel.files.Len() == 1 && c.grandparents.SizeSum() <= c.maxOverlapBytes { - // This compaction can be converted into a trivial move from one level + // This compaction can be converted into a move or copy from one level // to the next. We avoid such a move if there is lots of overlapping // grandparent data. Otherwise, the move could create a parent file // that will require a very expensive merge later on. - c.kind = compactionKindMove + iter := c.startLevel.files.Iter() + meta := iter.First() + isRemote := false + // We should always be passed a provider, except in some unit tests. + if provider != nil { + objMeta, err := provider.Lookup(fileTypeTable, meta.FileBacking.DiskFileNum) + if err != nil { + panic(errors.Wrapf(err, "cannot lookup table %s in provider", meta.FileBacking.DiskFileNum)) + } + isRemote = objMeta.IsRemote() + } + // Avoid a trivial move or copy if all of these are true, as rewriting a + // new file is better: + // + // 1) The source file is a virtual sstable + // 2) The existing file `meta` is on non-remote storage + // 3) The output level prefers shared storage + mustCopy := !isRemote && remote.ShouldCreateShared(opts.Experimental.CreateOnShared, c.outputLevel.level) + if mustCopy { + // If the source is virtual, it's best to just rewrite the file as all + // conditions in the above comment are met. + if !meta.Virtual { + c.kind = compactionKindCopy + } + } else { + c.kind = compactionKindMove + } } return c } -func newDeleteOnlyCompaction(opts *Options, cur *version, inputs []compactionLevel) *compaction { +func newDeleteOnlyCompaction( + opts *Options, cur *version, inputs []compactionLevel, beganAt time.Time, +) *compaction { c := &compaction{ kind: compactionKindDeleteOnly, cmp: opts.Comparer.Compare, @@ -736,6 +804,7 @@ func newDeleteOnlyCompaction(opts *Options, cur *version, inputs []compactionLev formatKey: opts.Comparer.FormatKey, logger: opts.Logger, version: cur, + beganAt: beganAt, inputs: inputs, } @@ -815,7 +884,9 @@ func adjustGrandparentOverlapBytesForFlush(c *compaction, flushingBytes uint64) } } -func newFlush(opts *Options, cur *version, baseLevel int, flushing flushableList) *compaction { +func newFlush( + opts *Options, cur *version, baseLevel int, flushing flushableList, beganAt time.Time, +) *compaction { c := &compaction{ kind: compactionKindFlush, cmp: opts.Comparer.Compare, @@ -824,6 +895,7 @@ func newFlush(opts *Options, cur *version, baseLevel int, flushing flushableList formatKey: opts.Comparer.FormatKey, logger: opts.Logger, version: cur, + beganAt: beganAt, inputs: []compactionLevel{{level: -1}, {level: 0}}, maxOutputFileSize: math.MaxUint64, maxOverlapBytes: math.MaxUint64, @@ -1210,92 +1282,69 @@ func (c *compaction) elideRangeKey(start, end []byte) bool { func (c *compaction) newInputIter( newIters tableNewIters, newRangeKeyIter keyspan.TableNewSpanIter, snapshots []uint64, ) (_ internalIterator, retErr error) { - var rangeDelIters []keyspan.FragmentIterator - var rangeKeyIters []keyspan.FragmentIterator - - if len(c.flushing) != 0 { - if len(c.flushing) == 1 { - f := c.flushing[0] - iter := f.newFlushIter(nil, &c.bytesIterated) - if rangeDelIter := f.newRangeDelIter(nil); rangeDelIter != nil { - c.rangeDelIter.Init(c.cmp, rangeDelIter) - iter = newMergingIter(c.logger, &c.stats, c.cmp, nil, iter, &c.rangeDelIter) - } - if rangeKeyIter := f.newRangeKeyIter(nil); rangeKeyIter != nil { - mi := &keyspan.MergingIter{} - mi.Init(c.cmp, rangeKeyCompactionTransform(c.equal, snapshots, c.elideRangeKey), new(keyspan.MergingBuffers), rangeKeyIter) - c.rangeKeyInterleaving.Init(c.comparer, iter, mi, nil /* hooks */, nil /* lowerBound */, nil /* upperBound */) - iter = &c.rangeKeyInterleaving - } - return iter, nil - } - iters := make([]internalIterator, 0, len(c.flushing)+1) - rangeDelIters = make([]keyspan.FragmentIterator, 0, len(c.flushing)) - rangeKeyIters = make([]keyspan.FragmentIterator, 0, len(c.flushing)) - for i := range c.flushing { - f := c.flushing[i] - iters = append(iters, f.newFlushIter(nil, &c.bytesIterated)) - rangeDelIter := f.newRangeDelIter(nil) - if rangeDelIter != nil { - rangeDelIters = append(rangeDelIters, rangeDelIter) - } - if rangeKeyIter := f.newRangeKeyIter(nil); rangeKeyIter != nil { - rangeKeyIters = append(rangeKeyIters, rangeKeyIter) + // Validate the ordering of compaction input files for defense in depth. + // TODO(jackson): Some of the CheckOrdering calls may be adapted to pass + // ProhibitSplitUserKeys if we thread the active format major version in. Or + // if we remove support for earlier FMVs, we can remove the parameter + // altogether. + if len(c.flushing) == 0 { + if c.startLevel.level >= 0 { + err := manifest.CheckOrdering(c.cmp, c.formatKey, + manifest.Level(c.startLevel.level), c.startLevel.files.Iter(), + manifest.AllowSplitUserKeys) + if err != nil { + return nil, err } } - if len(rangeDelIters) > 0 { - c.rangeDelIter.Init(c.cmp, rangeDelIters...) - iters = append(iters, &c.rangeDelIter) - } - var iter internalIterator = newMergingIter(c.logger, &c.stats, c.cmp, nil, iters...) - if len(rangeKeyIters) > 0 { - mi := &keyspan.MergingIter{} - mi.Init(c.cmp, rangeKeyCompactionTransform(c.equal, snapshots, c.elideRangeKey), new(keyspan.MergingBuffers), rangeKeyIters...) - c.rangeKeyInterleaving.Init(c.comparer, iter, mi, nil /* hooks */, nil /* lowerBound */, nil /* upperBound */) - iter = &c.rangeKeyInterleaving - } - return iter, nil - } - - if c.startLevel.level >= 0 { err := manifest.CheckOrdering(c.cmp, c.formatKey, - manifest.Level(c.startLevel.level), c.startLevel.files.Iter()) + manifest.Level(c.outputLevel.level), c.outputLevel.files.Iter(), + manifest.AllowSplitUserKeys) if err != nil { return nil, err } - } - err := manifest.CheckOrdering(c.cmp, c.formatKey, - manifest.Level(c.outputLevel.level), c.outputLevel.files.Iter()) - if err != nil { - return nil, err - } - - if c.startLevel.level == 0 { - if c.l0SublevelInfo == nil { - panic("l0SublevelInfo not created for compaction out of L0") + if c.startLevel.level == 0 { + if c.startLevel.l0SublevelInfo == nil { + panic("l0SublevelInfo not created for compaction out of L0") + } + for _, info := range c.startLevel.l0SublevelInfo { + err := manifest.CheckOrdering(c.cmp, c.formatKey, + info.sublevel, info.Iter(), + // NB: L0 sublevels have never allowed split user keys. + manifest.ProhibitSplitUserKeys) + if err != nil { + return nil, err + } + } } - - for _, info := range c.l0SublevelInfo { + if len(c.extraLevels) > 0 { + if len(c.extraLevels) > 1 { + panic("n>2 multi level compaction not implemented yet") + } + interLevel := c.extraLevels[0] err := manifest.CheckOrdering(c.cmp, c.formatKey, - info.sublevel, info.Iter()) + manifest.Level(interLevel.level), interLevel.files.Iter(), + manifest.AllowSplitUserKeys) if err != nil { return nil, err } } } - if len(c.extraLevels) > 0 { - if len(c.extraLevels) > 1 { - panic("n>2 multi level compaction not implemented yet") - } - interLevel := c.extraLevels[0] - err := manifest.CheckOrdering(c.cmp, c.formatKey, - manifest.Level(interLevel.level), interLevel.files.Iter()) - if err != nil { - return nil, err - } - } - iters := make([]internalIterator, 0, len(c.inputs)*c.startLevel.files.Len()+1) + // There are three classes of keys that a compaction needs to process: point + // keys, range deletion tombstones and range keys. Collect all iterators for + // all these classes of keys from all the levels. We'll aggregate them + // together farther below. + // + // numInputLevels is an approximation of the number of iterator levels. Due + // to idiosyncrasies in iterator construction, we may (rarely) exceed this + // initial capacity. + numInputLevels := max[int](len(c.flushing), len(c.inputs)) + iters := make([]internalIterator, 0, numInputLevels) + rangeDelIters := make([]keyspan.FragmentIterator, 0, numInputLevels) + rangeKeyIters := make([]keyspan.FragmentIterator, 0, numInputLevels) + + // If construction of the iterator inputs fails, ensure that we close all + // the consitutent iterators. defer func() { if retErr != nil { for _, iter := range iters { @@ -1308,161 +1357,103 @@ func (c *compaction) newInputIter( } } }() - - // In normal operation, levelIter iterates over the point operations in a - // level, and initializes a rangeDelIter pointer for the range deletions in - // each table. During compaction, we want to iterate over the merged view of - // point operations and range deletions. In order to do this we create one - // levelIter per level to iterate over the point operations, and collect up - // all the range deletion files. - // - // The range deletion levels are first combined with a keyspan.MergingIter - // (currently wrapped by a keyspan.InternalIteratorShim to satisfy the - // internal iterator interface). The resulting merged rangedel iterator is - // then included with the point levels in a single mergingIter. - newRangeDelIter := func( - f manifest.LevelFile, _ *IterOptions, bytesIterated *uint64, - ) (keyspan.FragmentIterator, error) { - iter, rangeDelIter, err := newIters(context.Background(), f.FileMetadata, - nil /* iter options */, internalIterOpts{bytesIterated: &c.bytesIterated}) - if err == nil { - // TODO(peter): It is mildly wasteful to open the point iterator only to - // immediately close it. One way to solve this would be to add new - // methods to tableCache for creating point and range-deletion iterators - // independently. We'd only want to use those methods here, - // though. Doesn't seem worth the hassle in the near term. - if err = iter.Close(); err != nil { - rangeDelIter.Close() - rangeDelIter = nil - } - } - if rangeDelIter != nil { - // Ensure that rangeDelIter is not closed until the compaction is - // finished. This is necessary because range tombstone processing - // requires the range tombstones to be held in memory for up to the - // lifetime of the compaction. - c.closers = append(c.closers, rangeDelIter) - rangeDelIter = noCloseIter{rangeDelIter} - - // Truncate the range tombstones returned by the iterator to the - // upper bound of the atomic compaction unit of the file. We want to - // truncate the range tombstone to the bounds of the file, but files - // with split user keys pose an obstacle: The file's largest bound - // is inclusive whereas the range tombstone's end is exclusive. - // - // Consider the example: - // - // 000001:[b-f#200] range del [c,k) - // 000002:[f#190-g#inf] range del [c,k) - // 000003:[g#500-i#3] - // - // Files 000001 and 000002 contain the untruncated range tombstones - // [c,k). While the keyspace covered by 000003 was at one point - // deleted by the tombstone [c,k), the tombstone may have already - // been compacted away and the file does not contain an untruncated - // range tombstone. We want to bound 000001's tombstone to the file - // bounds, but it's not possible to encode a range tombstone with an - // end boundary within a user key (eg, between sequence numbers - // f#200 and f#190). Instead, we expand 000001 to its atomic - // compaction unit (000001 and 000002) and truncate the tombstone to - // g#inf. - // - // NB: We must not use the atomic compaction unit of the entire - // compaction, because the [c,k) tombstone contained in the file - // 000001 ≥ g. If 000001, 000002 and 000003 are all included in the - // same compaction, the compaction's atomic compaction unit includes - // 000003. However 000003's keys must not be covered by 000001's - // untruncated range tombstone. - // - // Note that we need do this truncation at read time in order to - // handle sstables generated by RocksDB and earlier versions of - // Pebble which do not truncate range tombstones to atomic - // compaction unit boundaries at write time. - // - // The current Pebble compaction logic DOES truncate tombstones to - // atomic unit boundaries at compaction time too. - atomicUnit, _ := expandToAtomicUnit(c.cmp, f.Slice(), true /* disableIsCompacting */) - lowerBound, upperBound := manifest.KeyRange(c.cmp, atomicUnit.Iter()) - // Range deletion tombstones are often written to sstables - // untruncated on the end key side. However, they are still only - // valid within a given file's bounds. The logic for writing range - // tombstones to an output file sometimes has an incomplete view - // of range tombstones outside the file's internal key bounds. Skip - // any range tombstones completely outside file bounds. - rangeDelIter = keyspan.Truncate( - c.cmp, rangeDelIter, lowerBound.UserKey, upperBound.UserKey, &f.Smallest, &f.Largest) - } - if rangeDelIter == nil { - rangeDelIter = emptyKeyspanIter - } - return rangeDelIter, err - } - iterOpts := IterOptions{logger: c.logger} - // TODO(bananabrick): Get rid of the extra manifest.Level parameter and fold it into - // compactionLevel. - addItersForLevel := func(level *compactionLevel, l manifest.Level) error { - iters = append(iters, newLevelIter(iterOpts, c.cmp, nil /* split */, newIters, - level.files.Iter(), l, &c.bytesIterated)) - // TODO(jackson): Use keyspan.LevelIter to avoid loading all the range - // deletions into memory upfront. (See #2015, which reverted this.) - // There will be no user keys that are split between sstables - // within a level in Cockroach 23.1, which unblocks this optimization. - // Add the range deletion iterator for each file as an independent level - // in mergingIter, as opposed to making a levelIter out of those. This - // is safer as levelIter expects all keys coming from underlying - // iterators to be in order. Due to compaction / tombstone writing - // logic in finishOutput(), it is possible for range tombstones to not - // be strictly ordered across all files in one level. - // - // Consider this example from the metamorphic tests (also repeated in - // finishOutput()), consisting of three L3 files with their bounds - // specified in square brackets next to the file name: - // - // ./000240.sst [tmgc#391,MERGE-tmgc#391,MERGE] - // tmgc#391,MERGE [786e627a] - // tmgc-udkatvs#331,RANGEDEL - // - // ./000241.sst [tmgc#384,MERGE-tmgc#384,MERGE] - // tmgc#384,MERGE [666c7070] - // tmgc-tvsalezade#383,RANGEDEL - // tmgc-tvsalezade#331,RANGEDEL - // - // ./000242.sst [tmgc#383,RANGEDEL-tvsalezade#72057594037927935,RANGEDEL] - // tmgc-tvsalezade#383,RANGEDEL - // tmgc#375,SET [72646c78766965616c72776865676e79] - // tmgc-tvsalezade#356,RANGEDEL - // - // Here, the range tombstone in 000240.sst falls "after" one in - // 000241.sst, despite 000240.sst being ordered "before" 000241.sst for - // levelIter's purposes. While each file is still consistent before its - // bounds, it's safer to have all rangedel iterators be visible to - // mergingIter. - iter := level.files.Iter() - for f := iter.First(); f != nil; f = iter.Next() { - rangeDelIter, err := newRangeDelIter(iter.Take(), nil, &c.bytesIterated) - if err != nil { - return errors.Wrapf(err, "pebble: could not open table %s", errors.Safe(f.FileNum)) - } - if rangeDelIter != emptyKeyspanIter { + // Populate iters, rangeDelIters and rangeKeyIters with the appropriate + // constituent iterators. This depends on whether this is a flush or a + // compaction. + if len(c.flushing) != 0 { + // If flushing, we need to build the input iterators over the memtables + // stored in c.flushing. + for i := range c.flushing { + f := c.flushing[i] + iters = append(iters, f.newFlushIter(nil, &c.bytesIterated)) + rangeDelIter := f.newRangeDelIter(nil) + if rangeDelIter != nil { rangeDelIters = append(rangeDelIters, rangeDelIter) } - } - - // Check if this level has any range keys. - hasRangeKeys := false - for f := iter.First(); f != nil; f = iter.Next() { - if f.HasRangeKeys { - hasRangeKeys = true - break + if rangeKeyIter := f.newRangeKeyIter(nil); rangeKeyIter != nil { + rangeKeyIters = append(rangeKeyIters, rangeKeyIter) } } - if hasRangeKeys { - li := &keyspan.LevelIter{} - newRangeKeyIterWrapper := func(file *manifest.FileMetadata, iterOptions *keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) { - iter, err := newRangeKeyIter(file, iterOptions) - if iter != nil { + } else { + addItersForLevel := func(level *compactionLevel, l manifest.Level) error { + // Add a *levelIter for point iterators. Because we don't call + // initRangeDel, the levelIter will close and forget the range + // deletion iterator when it steps on to a new file. Surfacing range + // deletions to compactions are handled below. + iters = append(iters, newLevelIter(iterOpts, c.comparer, newIters, + level.files.Iter(), l, internalIterOpts{ + bytesIterated: &c.bytesIterated, + bufferPool: &c.bufferPool, + })) + // TODO(jackson): Use keyspan.LevelIter to avoid loading all the range + // deletions into memory upfront. (See #2015, which reverted this.) + // There will be no user keys that are split between sstables + // within a level in Cockroach 23.1, which unblocks this optimization. + + // Add the range deletion iterator for each file as an independent level + // in mergingIter, as opposed to making a levelIter out of those. This + // is safer as levelIter expects all keys coming from underlying + // iterators to be in order. Due to compaction / tombstone writing + // logic in finishOutput(), it is possible for range tombstones to not + // be strictly ordered across all files in one level. + // + // Consider this example from the metamorphic tests (also repeated in + // finishOutput()), consisting of three L3 files with their bounds + // specified in square brackets next to the file name: + // + // ./000240.sst [tmgc#391,MERGE-tmgc#391,MERGE] + // tmgc#391,MERGE [786e627a] + // tmgc-udkatvs#331,RANGEDEL + // + // ./000241.sst [tmgc#384,MERGE-tmgc#384,MERGE] + // tmgc#384,MERGE [666c7070] + // tmgc-tvsalezade#383,RANGEDEL + // tmgc-tvsalezade#331,RANGEDEL + // + // ./000242.sst [tmgc#383,RANGEDEL-tvsalezade#72057594037927935,RANGEDEL] + // tmgc-tvsalezade#383,RANGEDEL + // tmgc#375,SET [72646c78766965616c72776865676e79] + // tmgc-tvsalezade#356,RANGEDEL + // + // Here, the range tombstone in 000240.sst falls "after" one in + // 000241.sst, despite 000240.sst being ordered "before" 000241.sst for + // levelIter's purposes. While each file is still consistent before its + // bounds, it's safer to have all rangedel iterators be visible to + // mergingIter. + iter := level.files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + rangeDelIter, closer, err := c.newRangeDelIter(newIters, iter.Take(), nil, l, &c.bytesIterated) + if err != nil { + // The error will already be annotated with the BackingFileNum, so + // we annotate it with the FileNum. + return errors.Wrapf(err, "pebble: could not open table %s", errors.Safe(f.FileNum)) + } + if rangeDelIter == nil { + continue + } + rangeDelIters = append(rangeDelIters, rangeDelIter) + c.closers = append(c.closers, closer) + } + + // Check if this level has any range keys. + hasRangeKeys := false + for f := iter.First(); f != nil; f = iter.Next() { + if f.HasRangeKeys { + hasRangeKeys = true + break + } + } + if hasRangeKeys { + li := &keyspan.LevelIter{} + newRangeKeyIterWrapper := func(file *manifest.FileMetadata, iterOptions keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) { + iter, err := newRangeKeyIter(file, iterOptions) + if err != nil { + return nil, err + } else if iter == nil { + return emptyKeyspanIter, nil + } // Ensure that the range key iter is not closed until the compaction is // finished. This is necessary because range key processing // requires the range keys to be held in memory for up to the @@ -1475,39 +1466,46 @@ func (c *compaction) newInputIter( // range tombstones. This is because range keys were added after we // stopped splitting user keys across sstables, so all the range keys // in this sstable must wholly lie within the file's bounds. + return iter, err } - if iter == nil { - iter = emptyKeyspanIter - } - return iter, err + li.Init(keyspan.SpanIterOptions{}, c.cmp, newRangeKeyIterWrapper, level.files.Iter(), l, manifest.KeyTypeRange) + rangeKeyIters = append(rangeKeyIters, li) } - li.Init(keyspan.SpanIterOptions{}, c.cmp, newRangeKeyIterWrapper, level.files.Iter(), l, manifest.KeyTypeRange) - rangeKeyIters = append(rangeKeyIters, li) + return nil } - return nil - } - if c.startLevel.level != 0 { - if err = addItersForLevel(c.startLevel, manifest.Level(c.startLevel.level)); err != nil { - return nil, err - } - } else { - for _, info := range c.l0SublevelInfo { - if err = addItersForLevel( - &compactionLevel{0, info.LevelSlice}, info.sublevel); err != nil { + for i := range c.inputs { + // If the level is annotated with l0SublevelInfo, expand it into one + // level per sublevel. + // TODO(jackson): Perform this expansion even earlier when we pick the + // compaction? + if len(c.inputs[i].l0SublevelInfo) > 0 { + for _, info := range c.startLevel.l0SublevelInfo { + sublevelCompactionLevel := &compactionLevel{0, info.LevelSlice, nil} + if err := addItersForLevel(sublevelCompactionLevel, info.sublevel); err != nil { + return nil, err + } + } + continue + } + if err := addItersForLevel(&c.inputs[i], manifest.Level(c.inputs[i].level)); err != nil { return nil, err } } } - if len(c.extraLevels) > 0 { - if err = addItersForLevel(c.extraLevels[0], manifest.Level(c.extraLevels[0].level)); err != nil { - return nil, err - } - } - if err = addItersForLevel(c.outputLevel, manifest.Level(c.outputLevel.level)); err != nil { - return nil, err - } + // In normal operation, levelIter iterates over the point operations in a + // level, and initializes a rangeDelIter pointer for the range deletions in + // each table. During compaction, we want to iterate over the merged view of + // point operations and range deletions. In order to do this we create one + // levelIter per level to iterate over the point operations, and collect up + // all the range deletion files. + // + // The range deletion levels are first combined with a keyspan.MergingIter + // (currently wrapped by a keyspan.InternalIteratorShim to satisfy the + // internal iterator interface). The resulting merged rangedel iterator is + // then included with the point levels in a single mergingIter. + // // Combine all the rangedel iterators using a keyspan.MergingIterator and a // InternalIteratorShim so that the range deletions may be interleaved in // the compaction input. @@ -1517,17 +1515,116 @@ func (c *compaction) newInputIter( c.rangeDelIter.Init(c.cmp, rangeDelIters...) iters = append(iters, &c.rangeDelIter) } - pointKeyIter := newMergingIter(c.logger, &c.stats, c.cmp, nil, iters...) + + // If there's only one constituent point iterator, we can avoid the overhead + // of a *mergingIter. This is possible, for example, when performing a flush + // of a single memtable. Otherwise, combine all the iterators into a merging + // iter. + iter := iters[0] + if len(iters) > 0 { + iter = newMergingIter(c.logger, &c.stats, c.cmp, nil, iters...) + } + // If there are range key iterators, we need to combine them using + // keyspan.MergingIter, and then interleave them among the points. if len(rangeKeyIters) > 0 { mi := &keyspan.MergingIter{} mi.Init(c.cmp, rangeKeyCompactionTransform(c.equal, snapshots, c.elideRangeKey), new(keyspan.MergingBuffers), rangeKeyIters...) di := &keyspan.DefragmentingIter{} di.Init(c.comparer, mi, keyspan.DefragmentInternal, keyspan.StaticDefragmentReducer, new(keyspan.DefragmentingBuffers)) - c.rangeKeyInterleaving.Init(c.comparer, pointKeyIter, di, nil /* hooks */, nil /* lowerBound */, nil /* upperBound */) - return &c.rangeKeyInterleaving, nil + c.rangeKeyInterleaving.Init(c.comparer, iter, di, keyspan.InterleavingIterOpts{}) + iter = &c.rangeKeyInterleaving + } + return iter, nil +} + +func (c *compaction) newRangeDelIter( + newIters tableNewIters, + f manifest.LevelFile, + _ *IterOptions, + l manifest.Level, + bytesIterated *uint64, +) (keyspan.FragmentIterator, io.Closer, error) { + iter, rangeDelIter, err := newIters(context.Background(), f.FileMetadata, + &IterOptions{level: l}, internalIterOpts{ + bytesIterated: &c.bytesIterated, + bufferPool: &c.bufferPool, + }) + if err != nil { + return nil, nil, err + } + // TODO(peter): It is mildly wasteful to open the point iterator only to + // immediately close it. One way to solve this would be to add new + // methods to tableCache for creating point and range-deletion iterators + // independently. We'd only want to use those methods here, + // though. Doesn't seem worth the hassle in the near term. + if err = iter.Close(); err != nil { + if rangeDelIter != nil { + err = errors.CombineErrors(err, rangeDelIter.Close()) + } + return nil, nil, err + } + if rangeDelIter == nil { + // The file doesn't contain any range deletions. + return nil, nil, nil } - return pointKeyIter, nil + // Ensure that rangeDelIter is not closed until the compaction is + // finished. This is necessary because range tombstone processing + // requires the range tombstones to be held in memory for up to the + // lifetime of the compaction. + closer := rangeDelIter + rangeDelIter = noCloseIter{rangeDelIter} + + // Truncate the range tombstones returned by the iterator to the + // upper bound of the atomic compaction unit of the file. We want to + // truncate the range tombstone to the bounds of the file, but files + // with split user keys pose an obstacle: The file's largest bound + // is inclusive whereas the range tombstone's end is exclusive. + // + // Consider the example: + // + // 000001:[b-f#200] range del [c,k) + // 000002:[f#190-g#inf] range del [c,k) + // 000003:[g#500-i#3] + // + // Files 000001 and 000002 contain the untruncated range tombstones + // [c,k). While the keyspace covered by 000003 was at one point + // deleted by the tombstone [c,k), the tombstone may have already + // been compacted away and the file does not contain an untruncated + // range tombstone. We want to bound 000001's tombstone to the file + // bounds, but it's not possible to encode a range tombstone with an + // end boundary within a user key (eg, between sequence numbers + // f#200 and f#190). Instead, we expand 000001 to its atomic + // compaction unit (000001 and 000002) and truncate the tombstone to + // g#inf. + // + // NB: We must not use the atomic compaction unit of the entire + // compaction, because the [c,k) tombstone contained in the file + // 000001 ≥ g. If 000001, 000002 and 000003 are all included in the + // same compaction, the compaction's atomic compaction unit includes + // 000003. However 000003's keys must not be covered by 000001's + // untruncated range tombstone. + // + // Note that we need do this truncation at read time in order to + // handle sstables generated by RocksDB and earlier versions of + // Pebble which do not truncate range tombstones to atomic + // compaction unit boundaries at write time. + // + // The current Pebble compaction logic DOES truncate tombstones to + // atomic unit boundaries at compaction time too. + atomicUnit, _ := expandToAtomicUnit(c.cmp, f.Slice(), true /* disableIsCompacting */) + lowerBound, upperBound := manifest.KeyRange(c.cmp, atomicUnit.Iter()) + // Range deletion tombstones are often written to sstables + // untruncated on the end key side. However, they are still only + // valid within a given file's bounds. The logic for writing range + // tombstones to an output file sometimes has an incomplete view + // of range tombstones outside the file's internal key bounds. Skip + // any range tombstones completely outside file bounds. + rangeDelIter = keyspan.Truncate( + c.cmp, rangeDelIter, lowerBound.UserKey, upperBound.UserKey, + &f.Smallest, &f.Largest, false, /* panicOnUpperTruncate */ + ) + return rangeDelIter, closer, nil } func (c *compaction) String() string { @@ -1602,31 +1699,6 @@ func (d *DB) addInProgressCompaction(c *compaction) { d.opts.Logger.Fatalf("could not update state for compaction: %s", err) } } - - if false { - // TODO(peter): Do we want to keep this? It is useful for seeing the - // concurrent compactions/flushes that are taking place. Right now, this - // spams the logs and output to tests. Figure out a way to useful expose - // it. - strs := make([]string, 0, len(d.mu.compact.inProgress)) - for c := range d.mu.compact.inProgress { - var s string - if c.startLevel.level == -1 { - s = fmt.Sprintf("mem->L%d", c.outputLevel.level) - } else { - s = fmt.Sprintf("L%d->L%d:%.1f", c.startLevel.level, c.outputLevel.level, c.score) - } - strs = append(strs, s) - } - // This odd sorting function is intended to sort "mem" before "L*". - sort.Slice(strs, func(i, j int) bool { - if strs[i][0] == strs[j][0] { - return strs[i] < strs[j] - } - return strs[i] > strs[j] - }) - d.opts.Logger.Infof("compactions: %s", strings.Join(strs, " ")) - } } // Removes compaction markers from files in a compaction. The rollback parameter @@ -1636,7 +1708,8 @@ func (d *DB) addInProgressCompaction(c *compaction) { // DB.mu must be held when calling this method, however this method can drop and // re-acquire that mutex. All writes to the manifest for this compaction should // have completed by this point. -func (d *DB) removeInProgressCompaction(c *compaction, rollback bool) { +func (d *DB) clearCompactingState(c *compaction, rollback bool) { + c.versionEditApplied = true for _, cl := range c.inputs { iter := cl.files.Iter() for f := iter.First(); f != nil; f = iter.Next() { @@ -1660,8 +1733,6 @@ func (d *DB) removeInProgressCompaction(c *compaction, rollback bool) { f.IsIntraL0Compacting = false } } - delete(d.mu.compact.inProgress, c) - l0InProgress := inProgressL0Compactions(d.getInProgressCompactionInfoLocked(c)) func() { // InitCompactingFileInfo requires that no other manifest writes be @@ -1678,16 +1749,12 @@ func (d *DB) removeInProgressCompaction(c *compaction, rollback bool) { func (d *DB) calculateDiskAvailableBytes() uint64 { if space, err := d.opts.FS.GetDiskUsage(d.dirname); err == nil { - atomic.StoreUint64(&d.atomic.diskAvailBytes, space.AvailBytes) + d.diskAvailBytes.Store(space.AvailBytes) return space.AvailBytes } else if !errors.Is(err, vfs.ErrUnsupported) { d.opts.EventListener.BackgroundError(err) } - return atomic.LoadUint64(&d.atomic.diskAvailBytes) -} - -func (d *DB) getDiskAvailableBytesCached() uint64 { - return atomic.LoadUint64(&d.atomic.diskAvailBytes) + return d.diskAvailBytes.Load() } func (d *DB) getDeletionPacerInfo() deletionPacerInfo { @@ -1704,6 +1771,14 @@ func (d *DB) getDeletionPacerInfo() deletionPacerInfo { return pacerInfo } +// onObsoleteTableDelete is called to update metrics when an sstable is deleted. +func (d *DB) onObsoleteTableDelete(fileSize uint64) { + d.mu.Lock() + d.mu.versions.metrics.Table.ObsoleteCount-- + d.mu.versions.metrics.Table.ObsoleteSize -= fileSize + d.mu.Unlock() +} + // maybeScheduleFlush schedules a flush if necessary. // // d.mu must be held when calling this. @@ -1733,7 +1808,7 @@ func (d *DB) passedFlushThreshold() bool { if d.mu.mem.queue[n].flushForced { // A flush was forced. Pretend the memtable size is the configured // size. See minFlushSize below. - size += uint64(d.opts.MemTableSize) + size += d.opts.MemTableSize } else { size += d.mu.mem.queue[n].totalBytes() } @@ -1747,7 +1822,7 @@ func (d *DB) passedFlushThreshold() bool { // configured memtable size. This prevents flushing of memtables at startup // while we're undergoing the ramp period on the memtable size. See // DB.newMemTable(). - minFlushSize := uint64(d.opts.MemTableSize) / 2 + minFlushSize := d.opts.MemTableSize / 2 return size >= minFlushSize } @@ -1797,8 +1872,8 @@ func (d *DB) maybeScheduleDelayedFlush(tbl *memTable, dur time.Duration) { d.makeRoomForWrite(nil) } else { mem.flushForced = true - d.maybeScheduleFlush() } + d.maybeScheduleFlush() } }() } @@ -1850,26 +1925,58 @@ func (d *DB) runIngestFlush(c *compaction) (*manifest.VersionEdit, error) { ve := &versionEdit{} var level int var err error + var fileToSplit *fileMetadata + var ingestSplitFiles []ingestSplitFile for _, file := range c.flushing[0].flushable.(*ingestedFlushable).files { - level, err = ingestTargetLevel( - d.newIters, d.tableNewRangeKeyIter, iterOpts, d.cmp, - c.version, baseLevel, d.mu.compact.inProgress, file, + suggestSplit := d.opts.Experimental.IngestSplit != nil && d.opts.Experimental.IngestSplit() && + d.FormatMajorVersion() >= FormatVirtualSSTables + level, fileToSplit, err = ingestTargetLevel( + d.newIters, d.tableNewRangeKeyIter, iterOpts, d.opts.Comparer, + c.version, baseLevel, d.mu.compact.inProgress, file.FileMetadata, + suggestSplit, ) if err != nil { return nil, err } - ve.NewFiles = append(ve.NewFiles, newFileEntry{Level: level, Meta: file}) + ve.NewFiles = append(ve.NewFiles, newFileEntry{Level: level, Meta: file.FileMetadata}) + if fileToSplit != nil { + ingestSplitFiles = append(ingestSplitFiles, ingestSplitFile{ + ingestFile: file.FileMetadata, + splitFile: fileToSplit, + level: level, + }) + } levelMetrics := c.metrics[level] if levelMetrics == nil { levelMetrics = &LevelMetrics{} c.metrics[level] = levelMetrics } - levelMetrics.NumFiles++ - levelMetrics.Size += int64(file.Size) levelMetrics.BytesIngested += file.Size levelMetrics.TablesIngested++ } + updateLevelMetricsOnExcise := func(m *fileMetadata, level int, added []newFileEntry) { + levelMetrics := c.metrics[level] + if levelMetrics == nil { + levelMetrics = &LevelMetrics{} + c.metrics[level] = levelMetrics + } + levelMetrics.NumFiles-- + levelMetrics.Size -= int64(m.Size) + for i := range added { + levelMetrics.NumFiles++ + levelMetrics.Size += int64(added[i].Meta.Size) + } + } + + if len(ingestSplitFiles) > 0 { + ve.DeletedFiles = make(map[manifest.DeletedFileEntry]*manifest.FileMetadata) + replacedFiles := make(map[base.FileNum][]newFileEntry) + if err := d.ingestSplit(ve, updateLevelMetricsOnExcise, ingestSplitFiles, replacedFiles); err != nil { + return nil, err + } + } + return ve, nil } @@ -1909,6 +2016,7 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { // flushable batch in the same flush, since the memtable and flushableBatch // have the same logNum, the logNum invariant check below will trigger. var n, inputs int + var inputBytes uint64 var ingest bool for ; n < len(d.mu.mem.queue)-1; n++ { if f, ok := d.mu.mem.queue[n].flushable.(*ingestedFlushable); ok { @@ -1937,6 +2045,7 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { if !d.mu.mem.queue[n].readyForFlush() { break } + inputBytes += d.mu.mem.queue[n].inuseBytes() } if n == 0 { // None of the immutable memtables are ready for flushing. @@ -1963,20 +2072,21 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { } c := newFlush(d.opts, d.mu.versions.currentVersion(), - d.mu.versions.picker.getBaseLevel(), d.mu.mem.queue[:n]) + d.mu.versions.picker.getBaseLevel(), d.mu.mem.queue[:n], d.timeNow()) d.addInProgressCompaction(c) jobID := d.mu.nextJobID d.mu.nextJobID++ d.opts.EventListener.FlushBegin(FlushInfo{ - JobID: jobID, - Input: inputs, - Ingest: ingest, + JobID: jobID, + Input: inputs, + InputBytes: inputBytes, + Ingest: ingest, }) startTime := d.timeNow() var ve *manifest.VersionEdit - var pendingOutputs []*manifest.FileMetadata + var pendingOutputs []physicalMeta var stats compactStats // To determine the target level of the files in the ingestedFlushable, we // need to acquire the logLock, and not release it for that duration. Since, @@ -1997,12 +2107,13 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { } info := FlushInfo{ - JobID: jobID, - Input: inputs, - Duration: d.timeNow().Sub(startTime), - Done: true, - Ingest: ingest, - Err: err, + JobID: jobID, + Input: inputs, + InputBytes: inputBytes, + Duration: d.timeNow().Sub(startTime), + Done: true, + Ingest: ingest, + Err: err, } if err == nil { for i := range ve.NewFiles { @@ -2036,13 +2147,42 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { metrics.BytesIn += d.mu.mem.queue[i].logSize } } + } else if len(ve.DeletedFiles) > 0 { + // c.kind == compactionKindIngestedFlushable && we have deleted files due + // to ingest-time splits. + // + // Iterate through all other compactions, and check if their inputs have + // been replaced due to an ingest-time split. In that case, cancel the + // compaction. + for c2 := range d.mu.compact.inProgress { + for i := range c2.inputs { + iter := c2.inputs[i].files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if _, ok := ve.DeletedFiles[deletedFileEntry{FileNum: f.FileNum, Level: c2.inputs[i].level}]; ok { + c2.cancel.Store(true) + break + } + } + } + } } err = d.mu.versions.logAndApply(jobID, ve, c.metrics, false, /* forceRotation */ func() []compactionInfo { return d.getInProgressCompactionInfoLocked(c) }) if err != nil { info.Err = err // TODO(peter): untested. - d.mu.versions.obsoleteTables = append(d.mu.versions.obsoleteTables, pendingOutputs...) + for _, f := range pendingOutputs { + // Note that the FileBacking for the file metadata might not have + // been set yet. So, we directly use the FileNum. Since these + // files were generated as compaction outputs, these must be + // physical files on disk. This property might not hold once + // https://github.com/cockroachdb/pebble/issues/389 is + // implemented if #389 creates virtual sstables as output files. + d.mu.versions.obsoleteTables = append( + d.mu.versions.obsoleteTables, + fileInfo{f.FileNum.DiskFileNum(), f.Size}, + ) + } d.mu.versions.updateObsoleteTableMetricsLocked() } } else { @@ -2052,12 +2192,19 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { } bytesFlushed = c.bytesIterated - d.mu.snapshots.cumulativePinnedCount += stats.cumulativePinnedKeys - d.mu.snapshots.cumulativePinnedSize += stats.cumulativePinnedSize - d.maybeUpdateDeleteCompactionHints(c) - d.removeInProgressCompaction(c, err != nil) - d.mu.versions.incrementCompactions(c.kind, c.extraLevels) + // If err != nil, then the flush will be retried, and we will recalculate + // these metrics. + if err == nil { + d.mu.snapshots.cumulativePinnedCount += stats.cumulativePinnedKeys + d.mu.snapshots.cumulativePinnedSize += stats.cumulativePinnedSize + d.mu.versions.metrics.Keys.MissizedTombstonesCount += stats.countMissizedDels + d.maybeUpdateDeleteCompactionHints(c) + } + + d.clearCompactingState(c, err != nil) + delete(d.mu.compact.inProgress, c) + d.mu.versions.incrementCompactions(c.kind, c.extraLevels, c.pickerMetrics) var flushed flushableList if err == nil { @@ -2072,6 +2219,36 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { d.mu.versions.metrics.Flush.AsIngestTableCount += l.TablesIngested } } + + // Update if any eventually file-only snapshots have now transitioned to + // being file-only. + earliestUnflushedSeqNum := d.getEarliestUnflushedSeqNumLocked() + currentVersion := d.mu.versions.currentVersion() + for s := d.mu.snapshots.root.next; s != &d.mu.snapshots.root; { + if s.efos == nil { + s = s.next + continue + } + if base.Visible(earliestUnflushedSeqNum, s.efos.seqNum, InternalKeySeqNumMax) { + s = s.next + continue + } + if s.efos.excised.Load() { + // If a concurrent excise has happened that overlaps with one of the key + // ranges this snapshot is interested in, this EFOS cannot transition to + // a file-only snapshot as keys in that range could now be deleted. Move + // onto the next snapshot. + s = s.next + continue + } + currentVersion.Ref() + + // NB: s.efos.transitionToFileOnlySnapshot could close s, in which + // case s.next would be nil. Save it before calling it. + next := s.next + _ = s.efos.transitionToFileOnlySnapshot(currentVersion) + s = next + } } // Signal FlushEnd after installing the new readState. This helps for unit // tests that use the callback to trigger a read using an iterator with @@ -2088,14 +2265,9 @@ func (d *DB) flush1() (bytesFlushed uint64, err error) { flushed[i].readerUnrefLocked(true) } - d.deleteObsoleteFiles(jobID, false /* waitForOngoing */) + d.deleteObsoleteFiles(jobID) - // Mark all the memtables we flushed as flushed. Note that we do this last so - // that a synchronous call to DB.Flush() will not return until the deletion - // of obsolete files from this job have completed. This makes testing easier - // and provides similar behavior to manual compactions where the compaction - // is not marked as completed until the deletion of obsolete files job has - // completed. + // Mark all the memtables we flushed as flushed. for i := range flushed { close(flushed[i].flushed) } @@ -2164,6 +2336,7 @@ func (d *DB) maybeScheduleCompactionPicker( } env := compactionEnv{ + diskAvailBytes: d.diskAvailBytes.Load(), earliestSnapshotSeqNum: d.mu.snapshots.earliest(), earliestUnflushedSeqNum: d.getEarliestUnflushedSeqNumLocked(), } @@ -2180,7 +2353,7 @@ func (d *DB) maybeScheduleCompactionPicker( d.mu.compact.deletionHints = unresolvedHints if len(inputs) > 0 { - c := newDeleteOnlyCompaction(d.opts, v, inputs) + c := newDeleteOnlyCompaction(d.opts, v, inputs, d.timeNow()) d.mu.compact.compactingCount++ d.addInProgressCompaction(c) go d.compact(c, nil) @@ -2188,11 +2361,12 @@ func (d *DB) maybeScheduleCompactionPicker( } for len(d.mu.compact.manual) > 0 && d.mu.compact.compactingCount < maxConcurrentCompactions { + v := d.mu.versions.currentVersion() manual := d.mu.compact.manual[0] env.inProgressCompactions = d.getInProgressCompactionInfoLocked(nil) - pc, retryLater := d.mu.versions.picker.pickManual(env, manual) + pc, retryLater := pickManualCompaction(v, d.opts, env, d.mu.versions.picker.getBaseLevel(), manual) if pc != nil { - c := newCompaction(pc, d.opts) + c := newCompaction(pc, d.opts, d.timeNow(), d.ObjProvider()) d.mu.compact.manual = d.mu.compact.manual[1:] d.mu.compact.compactingCount++ d.addInProgressCompaction(c) @@ -2219,7 +2393,7 @@ func (d *DB) maybeScheduleCompactionPicker( if pc == nil { break } - c := newCompaction(pc, d.opts) + c := newCompaction(pc, d.opts, d.timeNow(), d.ObjProvider()) d.mu.compact.compactingCount++ d.addInProgressCompaction(c) go d.compact(c, nil) @@ -2509,6 +2683,13 @@ func (d *DB) compact(c *compaction, errChannel chan error) { d.opts.EventListener.BackgroundError(err) } d.mu.compact.compactingCount-- + delete(d.mu.compact.inProgress, c) + // Add this compaction's duration to the cumulative duration. NB: This + // must be atomic with the above removal of c from + // d.mu.compact.InProgress to ensure Metrics.Compact.Duration does not + // miss or double count a completing compaction's duration. + d.mu.compact.duration += d.timeNow().Sub(c.beganAt) + // The previous compaction may have produced too many files in a // level, so reschedule another compaction if needed. d.maybeScheduleCompaction() @@ -2537,13 +2718,40 @@ func (d *DB) compact1(c *compaction, errChannel chan error) (err error) { info.Duration = d.timeNow().Sub(startTime) if err == nil { - d.mu.versions.logLock() - err = d.mu.versions.logAndApply(jobID, ve, c.metrics, false /* forceRotation */, func() []compactionInfo { - return d.getInProgressCompactionInfoLocked(c) - }) + err = func() error { + var err error + d.mu.versions.logLock() + // Check if this compaction had a conflicting operation (eg. a d.excise()) + // that necessitates it restarting from scratch. Note that since we hold + // the manifest lock, we don't expect this bool to change its value + // as only the holder of the manifest lock will ever write to it. + if c.cancel.Load() { + err = firstError(err, ErrCancelledCompaction) + } + if err != nil { + // logAndApply calls logUnlock. If we didn't call it, we need to call + // logUnlock ourselves. + d.mu.versions.logUnlock() + return err + } + return d.mu.versions.logAndApply(jobID, ve, c.metrics, false /* forceRotation */, func() []compactionInfo { + return d.getInProgressCompactionInfoLocked(c) + }) + }() if err != nil { // TODO(peter): untested. - d.mu.versions.obsoleteTables = append(d.mu.versions.obsoleteTables, pendingOutputs...) + for _, f := range pendingOutputs { + // Note that the FileBacking for the file metadata might not have + // been set yet. So, we directly use the FileNum. Since these + // files were generated as compaction outputs, these must be + // physical files on disk. This property might not hold once + // https://github.com/cockroachdb/pebble/issues/389 is + // implemented if #389 creates virtual sstables as output files. + d.mu.versions.obsoleteTables = append( + d.mu.versions.obsoleteTables, + fileInfo{f.FileNum.DiskFileNum(), f.Size}, + ) + } d.mu.versions.updateObsoleteTableMetricsLocked() } } @@ -2555,17 +2763,19 @@ func (d *DB) compact1(c *compaction, errChannel chan error) (err error) { e := &ve.NewFiles[i] info.Output.Tables = append(info.Output.Tables, e.Meta.TableInfo()) } + d.mu.snapshots.cumulativePinnedCount += stats.cumulativePinnedKeys + d.mu.snapshots.cumulativePinnedSize += stats.cumulativePinnedSize + d.mu.versions.metrics.Keys.MissizedTombstonesCount += stats.countMissizedDels + d.maybeUpdateDeleteCompactionHints(c) } - d.mu.snapshots.cumulativePinnedCount += stats.cumulativePinnedKeys - d.mu.snapshots.cumulativePinnedSize += stats.cumulativePinnedSize - - d.maybeUpdateDeleteCompactionHints(c) - d.removeInProgressCompaction(c, err != nil) - d.mu.versions.incrementCompactions(c.kind, c.extraLevels) + // NB: clearing compacting state must occur before updating the read state; + // L0Sublevels initialization depends on it. + d.clearCompactingState(c, err != nil) + d.mu.versions.incrementCompactions(c.kind, c.extraLevels, c.pickerMetrics) d.mu.versions.incrementCompactionBytes(-c.bytesWritten) - info.TotalDuration = d.timeNow().Sub(startTime) + info.TotalDuration = d.timeNow().Sub(c.beganAt) d.opts.EventListener.CompactionEnd(info) // Update the read state before deleting obsolete files because the @@ -2576,7 +2786,7 @@ func (d *DB) compact1(c *compaction, errChannel chan error) (err error) { d.updateReadStateLocked(d.opts.DebugCheck) d.updateTableStatsLocked(ve.NewFiles) } - d.deleteObsoleteFiles(jobID, true /* waitForOngoing */) + d.deleteObsoleteFiles(jobID) return err } @@ -2584,6 +2794,78 @@ func (d *DB) compact1(c *compaction, errChannel chan error) (err error) { type compactStats struct { cumulativePinnedKeys uint64 cumulativePinnedSize uint64 + countMissizedDels uint64 +} + +// runCopyCompaction runs a copy compaction where a new FileNum is created that +// is a byte-for-byte copy of the input file. This is used in lieu of a move +// compaction when a file is being moved across the local/remote storage +// boundary. +// +// d.mu must be held when calling this method. +func (d *DB) runCopyCompaction( + jobID int, + c *compaction, + meta *fileMetadata, + objMeta objstorage.ObjectMetadata, + versionEdit *versionEdit, +) (ve *versionEdit, pendingOutputs []physicalMeta, retErr error) { + ve = versionEdit + if objMeta.IsRemote() || !remote.ShouldCreateShared(d.opts.Experimental.CreateOnShared, c.outputLevel.level) { + panic("pebble: scheduled a copy compaction that is not actually moving files to shared storage") + } + // Note that based on logic in the compaction picker, we're guaranteed + // meta.Virtual is false. + if meta.Virtual { + panic(errors.AssertionFailedf("cannot do a copy compaction of a virtual sstable across local/remote storage")) + } + // We are in the relatively more complex case where we need to copy this + // file to remote/shared storage. Drop the db mutex while we do the + // copy. + // + // To ease up cleanup of the local file and tracking of refs, we create + // a new FileNum. This has the potential of making the block cache less + // effective, however. + metaCopy := new(fileMetadata) + *metaCopy = fileMetadata{ + Size: meta.Size, + CreationTime: meta.CreationTime, + SmallestSeqNum: meta.SmallestSeqNum, + LargestSeqNum: meta.LargestSeqNum, + Stats: meta.Stats, + Virtual: meta.Virtual, + } + if meta.HasPointKeys { + metaCopy.ExtendPointKeyBounds(c.cmp, meta.SmallestPointKey, meta.LargestPointKey) + } + if meta.HasRangeKeys { + metaCopy.ExtendRangeKeyBounds(c.cmp, meta.SmallestRangeKey, meta.LargestRangeKey) + } + metaCopy.FileNum = d.mu.versions.getNextFileNum() + metaCopy.InitPhysicalBacking() + c.metrics = map[int]*LevelMetrics{ + c.outputLevel.level: { + BytesIn: meta.Size, + BytesCompacted: meta.Size, + TablesCompacted: 1, + }, + } + pendingOutputs = append(pendingOutputs, metaCopy.PhysicalMeta()) + + d.mu.Unlock() + defer d.mu.Lock() + _, err := d.objProvider.LinkOrCopyFromLocal(context.TODO(), d.opts.FS, + d.objProvider.Path(objMeta), fileTypeTable, metaCopy.FileBacking.DiskFileNum, + objstorage.CreateOptions{PreferSharedStorage: true}) + if err != nil { + return ve, pendingOutputs, err + } + ve.NewFiles[0].Meta = metaCopy + + if err := d.objProvider.Sync(); err != nil { + return nil, pendingOutputs, err + } + return ve, pendingOutputs, nil } // runCompactions runs a compaction that produces new on-disk tables from @@ -2593,7 +2875,7 @@ type compactStats struct { // re-acquired during the course of this method. func (d *DB) runCompaction( jobID int, c *compaction, -) (ve *versionEdit, pendingOutputs []*fileMetadata, stats compactStats, retErr error) { +) (ve *versionEdit, pendingOutputs []physicalMeta, stats compactStats, retErr error) { // As a sanity check, confirm that the smallest / largest keys for new and // deleted files in the new versionEdit pass a validation function before // returning the edit. @@ -2620,8 +2902,6 @@ func (d *DB) runCompaction( levelMetrics := &LevelMetrics{} iter := cl.files.Iter() for f := iter.First(); f != nil; f = iter.Next() { - levelMetrics.NumFiles-- - levelMetrics.Size -= int64(f.Size) ve.DeletedFiles[deletedFileEntry{ Level: cl.level, FileNum: f.FileNum, @@ -2636,21 +2916,24 @@ func (d *DB) runCompaction( panic("pebble: runCompaction cannot handle compactionKindIngestedFlushable.") } - // Check for a trivial move of one table from one level to the next. We avoid + // Check for a move or copy of one table from one level to the next. We avoid // such a move if there is lots of overlapping grandparent data. Otherwise, // the move could create a parent file that will require a very expensive // merge later on. - if c.kind == compactionKindMove { + if c.kind == compactionKindMove || c.kind == compactionKindCopy { iter := c.startLevel.files.Iter() meta := iter.First() + if invariants.Enabled { + if iter.Next() != nil { + panic("got more than one file for a move or copy compaction") + } + } + objMeta, err := d.objProvider.Lookup(fileTypeTable, meta.FileBacking.DiskFileNum) + if err != nil { + return ve, pendingOutputs, stats, err + } c.metrics = map[int]*LevelMetrics{ - c.startLevel.level: { - NumFiles: -1, - Size: -int64(meta.Size), - }, c.outputLevel.level: { - NumFiles: 1, - Size: int64(meta.Size), BytesMoved: meta.Size, TablesMoved: 1, }, @@ -2663,6 +2946,12 @@ func (d *DB) runCompaction( {Level: c.outputLevel.level, Meta: meta}, }, } + if c.kind == compactionKindCopy { + ve, pendingOutputs, retErr = d.runCopyCompaction(jobID, c, meta, objMeta, ve) + if retErr != nil { + return ve, pendingOutputs, stats, retErr + } + } return ve, nil, stats, nil } @@ -2675,22 +2964,67 @@ func (d *DB) runCompaction( snapshots := d.mu.snapshots.toSlice() formatVers := d.FormatMajorVersion() + if c.flushing == nil { + // Before dropping the db mutex, grab a ref to the current version. This + // prevents any concurrent excises from deleting files that this compaction + // needs to read/maintain a reference to. + // + // Note that unlike user iterators, compactionIter does not maintain a ref + // of the version or read state. + vers := d.mu.versions.currentVersion() + vers.Ref() + defer vers.UnrefLocked() + } + + if c.cancel.Load() { + return ve, nil, stats, ErrCancelledCompaction + } + // Release the d.mu lock while doing I/O. // Note the unusual order: Unlock and then Lock. d.mu.Unlock() defer d.mu.Lock() + // Compactions use a pool of buffers to read blocks, avoiding polluting the + // block cache with blocks that will not be read again. We initialize the + // buffer pool with a size 12. This initial size does not need to be + // accurate, because the pool will grow to accommodate the maximum number of + // blocks allocated at a given time over the course of the compaction. But + // choosing a size larger than that working set avoids any additional + // allocations to grow the size of the pool over the course of iteration. + // + // Justification for initial size 12: In a two-level compaction, at any + // given moment we'll have 2 index blocks in-use and 2 data blocks in-use. + // Additionally, when decoding a compressed block, we'll temporarily + // allocate 1 additional block to hold the compressed buffer. In the worst + // case that all input sstables have two-level index blocks (+2), value + // blocks (+2), range deletion blocks (+n) and range key blocks (+n), we'll + // additionally require 2n+4 blocks where n is the number of input sstables. + // Range deletion and range key blocks are relatively rare, and the cost of + // an additional allocation or two over the course of the compaction is + // considered to be okay. A larger initial size would cause the pool to hold + // on to more memory, even when it's not in-use because the pool will + // recycle buffers up to the current capacity of the pool. The memory use of + // a 12-buffer pool is expected to be within reason, even if all the buffers + // grow to the typical size of an index block (256 KiB) which would + // translate to 3 MiB per compaction. + c.bufferPool.Init(12) + defer c.bufferPool.Release() + iiter, err := c.newInputIter(d.newIters, d.tableNewRangeKeyIter, snapshots) if err != nil { return nil, pendingOutputs, stats, err } c.allowedZeroSeqNum = c.allowZeroSeqNum() + iiter = invalidating.MaybeWrapIfInvariants(iiter) iter := newCompactionIter(c.cmp, c.equal, c.formatKey, d.merge, iiter, snapshots, &c.rangeDelFrag, &c.rangeKeyFrag, c.allowedZeroSeqNum, c.elideTombstone, - c.elideRangeTombstone, d.FormatMajorVersion()) + c.elideRangeTombstone, d.opts.Experimental.IneffectualSingleDeleteCallback, + d.opts.Experimental.SingleDeleteInvariantViolationCallback, + d.FormatMajorVersion()) var ( - createdFiles []base.FileNum + createdFiles []base.DiskFileNum tw *sstable.Writer pinnedKeySize uint64 pinnedValueSize uint64 @@ -2717,8 +3051,9 @@ func (d *DB) runCompaction( DeletedFiles: map[deletedFileEntry]*fileMetadata{}, } + startLevelBytes := c.startLevel.files.SizeSum() outputMetrics := &LevelMetrics{ - BytesIn: c.startLevel.files.SizeSum(), + BytesIn: startLevelBytes, BytesRead: c.outputLevel.files.SizeSum(), } if len(c.extraLevels) > 0 { @@ -2734,22 +3069,24 @@ func (d *DB) runCompaction( } if len(c.extraLevels) > 0 { c.metrics[c.extraLevels[0].level] = &LevelMetrics{} + outputMetrics.MultiLevel.BytesInTop = startLevelBytes + outputMetrics.MultiLevel.BytesIn = outputMetrics.BytesIn + outputMetrics.MultiLevel.BytesRead = outputMetrics.BytesRead } // The table is typically written at the maximum allowable format implied by // the current format major version of the DB. tableFormat := formatVers.MaxTableFormat() - if tableFormat > sstable.TableFormatPebblev3 { - // Since TableFormatPebblev3 does not currently subsume - // TableFormatPebblev2, this panic ensures that we have carefully thought - // through what we are doing before we introduce a format beyond - // TableFormatPebblev3. - panic("cannot handle table format beyond TableFormatPebblev3") - } + + // In format major versions with maximum table formats of Pebblev3, value + // blocks were conditional on an experimental setting. In format major + // versions with maximum table formats of Pebblev4 and higher, value blocks + // are always enabled. if tableFormat == sstable.TableFormatPebblev3 && (d.opts.Experimental.EnableValueBlocks == nil || !d.opts.Experimental.EnableValueBlocks()) { tableFormat = sstable.TableFormatPebblev2 } + writerOpts := d.opts.MakeWriterOptions(c.outputLevel.level, tableFormat) if formatVers < FormatBlockPropertyCollector { // Cannot yet write block properties. @@ -2773,14 +3110,34 @@ func (d *DB) runCompaction( }() newOutput := func() error { + // Check if we've been cancelled by a concurrent operation. + if c.cancel.Load() { + return ErrCancelledCompaction + } fileMeta := &fileMetadata{} d.mu.Lock() fileNum := d.mu.versions.getNextFileNum() fileMeta.FileNum = fileNum - pendingOutputs = append(pendingOutputs, fileMeta) + pendingOutputs = append(pendingOutputs, fileMeta.PhysicalMeta()) d.mu.Unlock() - writable, objMeta, err := d.objProvider.Create(fileTypeTable, fileNum, objstorage.CreateOptions{} /* TODO */) + ctx := context.TODO() + if objiotracing.Enabled { + ctx = objiotracing.WithLevel(ctx, c.outputLevel.level) + switch c.kind { + case compactionKindFlush: + ctx = objiotracing.WithReason(ctx, objiotracing.ForFlush) + case compactionKindIngestedFlushable: + ctx = objiotracing.WithReason(ctx, objiotracing.ForIngestion) + default: + ctx = objiotracing.WithReason(ctx, objiotracing.ForCompaction) + } + } + // Prefer shared storage if present. + createOpts := objstorage.CreateOptions{ + PreferSharedStorage: remote.ShouldCreateShared(d.opts.Experimental.CreateOnShared, c.outputLevel.level), + } + writable, objMeta, err := d.objProvider.Create(ctx, fileTypeTable, fileNum.DiskFileNum(), createOpts) if err != nil { return err } @@ -2802,8 +3159,8 @@ func (d *DB) runCompaction( written: &c.bytesWritten, } } - createdFiles = append(createdFiles, fileNum) - cacheOpts := private.SSTableCacheOpts(d.cacheID, fileNum).(sstable.WriterOption) + createdFiles = append(createdFiles, fileNum.DiskFileNum()) + cacheOpts := private.SSTableCacheOpts(d.cacheID, fileNum.DiskFileNum()).(sstable.WriterOption) const MaxFileWriteAdditionalCPUTime = time.Millisecond * 100 cpuWorkHandle = d.opts.Experimental.CPUWorkPermissionGranter.GetPermission( @@ -2944,9 +3301,13 @@ func (d *DB) runCompaction( meta.Size = writerMeta.Size meta.SmallestSeqNum = writerMeta.SmallestSeqNum meta.LargestSeqNum = writerMeta.LargestSeqNum + meta.InitPhysicalBacking() + // If the file didn't contain any range deletions, we can fill its // table stats now, avoiding unnecessarily loading the table later. - maybeSetStatsFromProperties(meta, &writerMeta.Properties) + maybeSetStatsFromProperties( + meta.PhysicalMeta(), &writerMeta.Properties, + ) if c.flushing == nil { outputMetrics.TablesCompacted++ @@ -3175,7 +3536,7 @@ func (d *DB) runCompaction( return nil, pendingOutputs, stats, err } } - if err := tw.Add(*key, val); err != nil { + if err := tw.AddWithForceObsolete(*key, val, iter.forceObsoleteDueToRangeDel); err != nil { return nil, pendingOutputs, stats, err } if iter.snapshotPinned { @@ -3219,8 +3580,6 @@ func (d *DB) runCompaction( for _, cl := range c.inputs { iter := cl.files.Iter() for f := iter.First(); f != nil; f = iter.Next() { - c.metrics[cl.level].NumFiles-- - c.metrics[cl.level].Size -= int64(f.Size) ve.DeletedFiles[deletedFileEntry{ Level: cl.level, FileNum: f.FileNum, @@ -3228,6 +3587,11 @@ func (d *DB) runCompaction( } } + // The compaction iterator keeps track of a count of the number of DELSIZED + // keys that encoded an incorrect size. Propagate it up as a part of + // compactStats. + stats.countMissizedDels = iter.stats.countMissizedDels + if err := d.objProvider.Sync(); err != nil { return nil, pendingOutputs, stats, err } @@ -3287,7 +3651,7 @@ func (d *DB) scanObsoleteFiles(list []string) { d.mu.compact.cond.Wait() } - liveFileNums := make(map[FileNum]struct{}) + liveFileNums := make(map[base.DiskFileNum]struct{}) d.mu.versions.addLiveFileNums(liveFileNums) // Protect against files which are only referred to by the ingestedFlushable // from being deleted. These are added to the flushable queue on WAL replay @@ -3297,7 +3661,7 @@ func (d *DB) scanObsoleteFiles(list []string) { for _, fEntry := range d.mu.mem.queue { if f, ok := fEntry.flushable.(*ingestedFlushable); ok { for _, file := range f.files { - liveFileNums[file.FileNum] = struct{}{} + liveFileNums[file.FileBacking.DiskFileNum] = struct{}{} } } } @@ -3306,39 +3670,39 @@ func (d *DB) scanObsoleteFiles(list []string) { manifestFileNum := d.mu.versions.manifestFileNum var obsoleteLogs []fileInfo - var obsoleteTables []*fileMetadata + var obsoleteTables []fileInfo var obsoleteManifests []fileInfo var obsoleteOptions []fileInfo for _, filename := range list { - fileType, fileNum, ok := base.ParseFilename(d.opts.FS, filename) + fileType, diskFileNum, ok := base.ParseFilename(d.opts.FS, filename) if !ok { continue } switch fileType { case fileTypeLog: - if fileNum >= minUnflushedLogNum { + if diskFileNum.FileNum() >= minUnflushedLogNum { continue } - fi := fileInfo{fileNum: fileNum} + fi := fileInfo{fileNum: diskFileNum} if stat, err := d.opts.FS.Stat(filename); err == nil { fi.fileSize = uint64(stat.Size()) } obsoleteLogs = append(obsoleteLogs, fi) case fileTypeManifest: - if fileNum >= manifestFileNum { + if diskFileNum.FileNum() >= manifestFileNum { continue } - fi := fileInfo{fileNum: fileNum} + fi := fileInfo{fileNum: diskFileNum} if stat, err := d.opts.FS.Stat(filename); err == nil { fi.fileSize = uint64(stat.Size()) } obsoleteManifests = append(obsoleteManifests, fi) case fileTypeOptions: - if fileNum >= d.optionsFileNum { + if diskFileNum.FileNum() >= d.optionsFileNum.FileNum() { continue } - fi := fileInfo{fileNum: fileNum} + fi := fileInfo{fileNum: diskFileNum} if stat, err := d.opts.FS.Stat(filename); err == nil { fi.fileSize = uint64(stat.Size()) } @@ -3354,16 +3718,16 @@ func (d *DB) scanObsoleteFiles(list []string) { for _, obj := range objects { switch obj.FileType { case fileTypeTable: - if _, ok := liveFileNums[obj.FileNum]; ok { + if _, ok := liveFileNums[obj.DiskFileNum]; ok { continue } - fileMeta := &fileMetadata{ - FileNum: obj.FileNum, + fileInfo := fileInfo{ + fileNum: obj.DiskFileNum, } if size, err := d.objProvider.Size(obj); err == nil { - fileMeta.Size = uint64(size) + fileInfo.fileSize = uint64(size) } - obsoleteTables = append(obsoleteTables, fileMeta) + obsoleteTables = append(obsoleteTables, fileInfo) default: // Ignore object types we don't know about. @@ -3372,7 +3736,7 @@ func (d *DB) scanObsoleteFiles(list []string) { d.mu.log.queue = merge(d.mu.log.queue, obsoleteLogs) d.mu.versions.metrics.WAL.Files = int64(len(d.mu.log.queue)) - d.mu.versions.obsoleteTables = mergeFileMetas(d.mu.versions.obsoleteTables, obsoleteTables) + d.mu.versions.obsoleteTables = mergeFileInfo(d.mu.versions.obsoleteTables, obsoleteTables) d.mu.versions.updateObsoleteTableMetricsLocked() d.mu.versions.obsoleteManifests = merge(d.mu.versions.obsoleteManifests, obsoleteManifests) d.mu.versions.obsoleteOptions = merge(d.mu.versions.obsoleteOptions, obsoleteOptions) @@ -3387,92 +3751,44 @@ func (d *DB) scanObsoleteFiles(list []string) { // // d.mu must be held when calling this method. func (d *DB) disableFileDeletions() { - d.mu.cleaner.disabled++ - for d.mu.cleaner.cleaning { - d.mu.cleaner.cond.Wait() - } - d.mu.cleaner.cond.Broadcast() + d.mu.disableFileDeletions++ + d.mu.Unlock() + defer d.mu.Lock() + d.cleanupManager.Wait() } -// enableFileDeletions enables previously disabled file deletions. Note that if -// file deletions have been re-enabled, the current goroutine will be used to -// perform the queued up deletions. +// enableFileDeletions enables previously disabled file deletions. A cleanup job +// is queued if necessary. // // d.mu must be held when calling this method. func (d *DB) enableFileDeletions() { - if d.mu.cleaner.disabled <= 0 || d.mu.cleaner.cleaning { + if d.mu.disableFileDeletions <= 0 { panic("pebble: file deletion disablement invariant violated") } - d.mu.cleaner.disabled-- - if d.mu.cleaner.disabled > 0 { + d.mu.disableFileDeletions-- + if d.mu.disableFileDeletions > 0 { return } jobID := d.mu.nextJobID d.mu.nextJobID++ - d.deleteObsoleteFiles(jobID, true /* waitForOngoing */) -} - -// d.mu must be held when calling this. -func (d *DB) acquireCleaningTurn(waitForOngoing bool) bool { - // Only allow a single delete obsolete files job to run at a time. - for d.mu.cleaner.cleaning && d.mu.cleaner.disabled == 0 && waitForOngoing { - d.mu.cleaner.cond.Wait() - } - if d.mu.cleaner.cleaning { - return false - } - if d.mu.cleaner.disabled > 0 { - // File deletions are currently disabled. When they are re-enabled a new - // job will be created to catch up on file deletions. - return false - } - d.mu.cleaner.cleaning = true - return true -} - -// d.mu must be held when calling this. -func (d *DB) releaseCleaningTurn() { - d.mu.cleaner.cleaning = false - d.mu.cleaner.cond.Broadcast() -} - -// deleteObsoleteFiles deletes those files and objects that are no longer -// needed. If waitForOngoing is true, it waits for any ongoing cleaning turns to -// complete, and if false, it returns rightaway if a cleaning turn is ongoing. -// -// d.mu must be held when calling this, but the mutex may be dropped and -// re-acquired during the course of this method. -func (d *DB) deleteObsoleteFiles(jobID int, waitForOngoing bool) { - if !d.acquireCleaningTurn(waitForOngoing) { - return - } - d.doDeleteObsoleteFiles(jobID) - d.releaseCleaningTurn() -} - -// obsoleteFile holds information about a file that needs to be deleted soon. -type obsoleteFile struct { - dir string - fileNum base.FileNum - fileType fileType - fileSize uint64 + d.deleteObsoleteFiles(jobID) } type fileInfo struct { - fileNum FileNum + fileNum base.DiskFileNum fileSize uint64 } -// d.mu must be held when calling this, but the mutex may be dropped and -// re-acquired during the course of this method. -func (d *DB) doDeleteObsoleteFiles(jobID int) { - var obsoleteTables []fileInfo - - defer func() { - for _, tbl := range obsoleteTables { - delete(d.mu.versions.zombieTables, tbl.fileNum) - } - }() +// deleteObsoleteFiles enqueues a cleanup job to the cleanup manager, if necessary. +// +// d.mu must be held when calling this. The function will release and re-aquire the mutex. +// +// Does nothing if file deletions are disabled (see disableFileDeletions). A +// cleanup job will be scheduled when file deletions are re-enabled. +func (d *DB) deleteObsoleteFiles(jobID int) { + if d.mu.disableFileDeletions > 0 { + return + } var obsoleteLogs []fileInfo for i := range d.mu.log.queue { @@ -3480,7 +3796,7 @@ func (d *DB) doDeleteObsoleteFiles(jobID int) { // log that has not had its contents flushed to an sstable. We can recycle // the prefix of d.mu.log.queue with log numbers less than // minUnflushedLogNum. - if d.mu.log.queue[i].fileNum >= d.mu.versions.minUnflushedLogNum { + if d.mu.log.queue[i].fileNum.FileNum() >= d.mu.versions.minUnflushedLogNum { obsoleteLogs = d.mu.log.queue[:i] d.mu.log.queue = d.mu.log.queue[i:] d.mu.versions.metrics.WAL.Files -= int64(len(obsoleteLogs)) @@ -3488,19 +3804,18 @@ func (d *DB) doDeleteObsoleteFiles(jobID int) { } } - for _, table := range d.mu.versions.obsoleteTables { - obsoleteTables = append(obsoleteTables, fileInfo{ - fileNum: table.FileNum, - fileSize: table.Size, - }) - } + obsoleteTables := append([]fileInfo(nil), d.mu.versions.obsoleteTables...) d.mu.versions.obsoleteTables = nil + for _, tbl := range obsoleteTables { + delete(d.mu.versions.zombieTables, tbl.fileNum) + } + // Sort the manifests cause we want to delete some contiguous prefix // of the older manifests. sort.Slice(d.mu.versions.obsoleteManifests, func(i, j int) bool { - return d.mu.versions.obsoleteManifests[i].fileNum < - d.mu.versions.obsoleteManifests[j].fileNum + return d.mu.versions.obsoleteManifests[i].fileNum.FileNum() < + d.mu.versions.obsoleteManifests[j].fileNum.FileNum() }) var obsoleteManifests []fileInfo @@ -3516,7 +3831,7 @@ func (d *DB) doDeleteObsoleteFiles(jobID int) { obsoleteOptions := d.mu.versions.obsoleteOptions d.mu.versions.obsoleteOptions = nil - // Release d.mu while doing I/O + // Release d.mu while preparing the cleanup job and possibly waiting. // Note the unusual order: Unlock and then Lock. d.mu.Unlock() defer d.mu.Lock() @@ -3531,12 +3846,12 @@ func (d *DB) doDeleteObsoleteFiles(jobID int) { {fileTypeOptions, obsoleteOptions}, } _, noRecycle := d.opts.Cleaner.(base.NeedsFileContents) - filesToDelete := make([]obsoleteFile, 0, len(files)) + filesToDelete := make([]obsoleteFile, 0, len(obsoleteLogs)+len(obsoleteTables)+len(obsoleteManifests)+len(obsoleteOptions)) for _, f := range files { // We sort to make the order of deletions deterministic, which is nice for // tests. sort.Slice(f.obsolete, func(i, j int) bool { - return f.obsolete[i].fileNum < f.obsolete[j].fileNum + return f.obsolete[i].fileNum.FileNum() < f.obsolete[j].fileNum.FileNum() }) for _, fi := range f.obsolete { dir := d.dirname @@ -3559,121 +3874,24 @@ func (d *DB) doDeleteObsoleteFiles(jobID int) { } } if len(filesToDelete) > 0 { - var tablesSize uint64 - for i := range filesToDelete { - if filesToDelete[i].fileType == fileTypeTable { - tablesSize += filesToDelete[i].fileSize - } - } - d.deletionPacer.reportDeletion(tablesSize) - d.deleters.Add(1) - // Delete asynchronously if that could get held up in the pacer. - if d.opts.Experimental.MinDeletionRate > 0 { - go d.paceAndDeleteObsoleteFiles(jobID, filesToDelete) - } else { - d.paceAndDeleteObsoleteFiles(jobID, filesToDelete) - } + d.cleanupManager.EnqueueJob(jobID, filesToDelete) } -} - -// Paces and eventually deletes the list of obsolete files passed in. db.mu -// must NOT be held when calling this method. -func (d *DB) paceAndDeleteObsoleteFiles(jobID int, files []obsoleteFile) { - defer d.deleters.Done() - - for _, of := range files { - path := base.MakeFilepath(d.opts.FS, of.dir, of.fileType, of.fileNum) - if of.fileType == fileTypeTable { - _ = d.deletionPacer.maybeThrottle(of.fileSize) - d.mu.Lock() - d.mu.versions.metrics.Table.ObsoleteCount-- - d.mu.versions.metrics.Table.ObsoleteSize -= of.fileSize - d.mu.Unlock() - d.deleteObsoleteObject(fileTypeTable, jobID, of.fileNum) - } else { - d.deleteObsoleteFile(of.fileType, jobID, path, of.fileNum) - } + if d.opts.private.testingAlwaysWaitForCleanup { + d.cleanupManager.Wait() } } func (d *DB) maybeScheduleObsoleteTableDeletion() { d.mu.Lock() defer d.mu.Unlock() - - if len(d.mu.versions.obsoleteTables) == 0 { - return - } - if !d.acquireCleaningTurn(false) { - return - } - - go func() { - pprof.Do(context.Background(), gcLabels, func(context.Context) { - d.mu.Lock() - defer d.mu.Unlock() - - jobID := d.mu.nextJobID - d.mu.nextJobID++ - d.doDeleteObsoleteFiles(jobID) - d.releaseCleaningTurn() - }) - }() + d.maybeScheduleObsoleteTableDeletionLocked() } -func (d *DB) deleteObsoleteObject(fileType fileType, jobID int, fileNum FileNum) { - if fileType != fileTypeTable { - panic("not an object") - } - - var path string - meta, err := d.objProvider.Lookup(fileType, fileNum) - if err != nil { - path = "" - } else { - path = d.objProvider.Path(meta) - err = d.objProvider.Remove(fileType, fileNum) - } - if objstorage.IsNotExistError(err) { - return - } - - switch fileType { - case fileTypeTable: - d.opts.EventListener.TableDeleted(TableDeleteInfo{ - JobID: jobID, - Path: path, - FileNum: fileNum, - Err: err, - }) - } -} - -// deleteObsoleteFile deletes a (non-object) file that is no longer needed. -func (d *DB) deleteObsoleteFile(fileType fileType, jobID int, path string, fileNum FileNum) { - // TODO(peter): need to handle this error, probably by re-adding the - // file that couldn't be deleted to one of the obsolete slices map. - err := d.opts.Cleaner.Clean(d.opts.FS, fileType, path) - if oserror.IsNotExist(err) { - return - } - - switch fileType { - case fileTypeLog: - d.opts.EventListener.WALDeleted(WALDeleteInfo{ - JobID: jobID, - Path: path, - FileNum: fileNum, - Err: err, - }) - case fileTypeManifest: - d.opts.EventListener.ManifestDeleted(ManifestDeleteInfo{ - JobID: jobID, - Path: path, - FileNum: fileNum, - Err: err, - }) - case fileTypeTable: - panic("invalid deletion of object file") +func (d *DB) maybeScheduleObsoleteTableDeletionLocked() { + if len(d.mu.versions.obsoleteTables) > 0 { + jobID := d.mu.nextJobID + d.mu.nextJobID++ + d.deleteObsoleteFiles(jobID) } } @@ -3684,7 +3902,7 @@ func merge(a, b []fileInfo) []fileInfo { a = append(a, b...) sort.Slice(a, func(i, j int) bool { - return a[i].fileNum < a[j].fileNum + return a[i].fileNum.FileNum() < a[j].fileNum.FileNum() }) n := 0 @@ -3697,22 +3915,29 @@ func merge(a, b []fileInfo) []fileInfo { return a[:n] } -func mergeFileMetas(a, b []*fileMetadata) []*fileMetadata { +func mergeFileInfo(a, b []fileInfo) []fileInfo { if len(b) == 0 { return a } a = append(a, b...) sort.Slice(a, func(i, j int) bool { - return a[i].FileNum < a[j].FileNum + return a[i].fileNum.FileNum() < a[j].fileNum.FileNum() }) n := 0 for i := 0; i < len(a); i++ { - if n == 0 || a[i].FileNum != a[n-1].FileNum { + if n == 0 || a[i].fileNum != a[n-1].fileNum { a[n] = a[i] n++ } } return a[:n] } + +func max[I constraints.Ordered](a, b I) I { + if b > a { + return b + } + return a +} diff --git a/vendor/github.com/cockroachdb/pebble/compaction_iter.go b/vendor/github.com/cockroachdb/pebble/compaction_iter.go index 8434fa6..299dbfc 100644 --- a/vendor/github.com/cockroachdb/pebble/compaction_iter.go +++ b/vendor/github.com/cockroachdb/pebble/compaction_iter.go @@ -6,6 +6,7 @@ package pebble import ( "bytes" + "encoding/binary" "fmt" "io" "sort" @@ -14,9 +15,9 @@ import ( "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/bytealloc" - "github.com/cockroachdb/pebble/internal/invariants" "github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/redact" ) // compactionIter provides a forward-only iterator that encapsulates the logic @@ -205,7 +206,29 @@ type compactionIter struct { // compaction iterator was only returned because an open snapshot prevents // its elision. This field only applies to point keys, and not to range // deletions or range keys. + // + // For MERGE, it is possible that doing the merge is interrupted even when + // the next point key is in the same stripe. This can happen if the loop in + // mergeNext gets interrupted by sameStripeNonSkippable. + // sameStripeNonSkippable occurs due to RANGEDELs that sort before + // SET/MERGE/DEL with the same seqnum, so the RANGEDEL does not necessarily + // delete the subsequent SET/MERGE/DEL keys. snapshotPinned bool + // forceObsoleteDueToRangeDel is set to true in a subset of the cases that + // snapshotPinned is true. This value is true when the point is obsolete due + // to a RANGEDEL but could not be deleted due to a snapshot. + // + // NB: it may seem that the additional cases that snapshotPinned captures + // are harmless in that they can also be used to mark a point as obsolete + // (it is merely a duplication of some logic that happens in + // Writer.AddWithForceObsolete), but that is not quite accurate as of this + // writing -- snapshotPinned originated in stats collection and for a + // sequence MERGE, SET, where the MERGE cannot merge with the (older) SET + // due to a snapshot, the snapshotPinned value for the SET is true. + // + // TODO(sumeer,jackson): improve the logic of snapshotPinned and reconsider + // whether we need forceObsoleteDueToRangeDel. + forceObsoleteDueToRangeDel bool // The index of the snapshot for the current key within the snapshots slice. curSnapshotIdx int curSnapshotSeqNum uint64 @@ -233,13 +256,19 @@ type compactionIter struct { // The fragmented range keys. rangeKeys []keyspan.Span // Byte allocator for the tombstone keys. - alloc bytealloc.A - allowZeroSeqNum bool - elideTombstone func(key []byte) bool - elideRangeTombstone func(start, end []byte) bool + alloc bytealloc.A + allowZeroSeqNum bool + elideTombstone func(key []byte) bool + elideRangeTombstone func(start, end []byte) bool + ineffectualSingleDeleteCallback func(userKey []byte) + singleDeleteInvariantViolationCallback func(userKey []byte) // The on-disk format major version. This informs the types of keys that // may be written to disk during a compaction. formatVersion FormatMajorVersion + stats struct { + // count of DELSIZED keys that were missized. + countMissizedDels uint64 + } } func newCompactionIter( @@ -254,20 +283,24 @@ func newCompactionIter( allowZeroSeqNum bool, elideTombstone func(key []byte) bool, elideRangeTombstone func(start, end []byte) bool, + ineffectualSingleDeleteCallback func(userKey []byte), + singleDeleteInvariantViolationCallback func(userKey []byte), formatVersion FormatMajorVersion, ) *compactionIter { i := &compactionIter{ - equal: equal, - merge: merge, - iter: iter, - snapshots: snapshots, - frontiers: frontiers{cmp: cmp}, - rangeDelFrag: rangeDelFrag, - rangeKeyFrag: rangeKeyFrag, - allowZeroSeqNum: allowZeroSeqNum, - elideTombstone: elideTombstone, - elideRangeTombstone: elideRangeTombstone, - formatVersion: formatVersion, + equal: equal, + merge: merge, + iter: iter, + snapshots: snapshots, + frontiers: frontiers{cmp: cmp}, + rangeDelFrag: rangeDelFrag, + rangeKeyFrag: rangeKeyFrag, + allowZeroSeqNum: allowZeroSeqNum, + elideTombstone: elideTombstone, + elideRangeTombstone: elideRangeTombstone, + ineffectualSingleDeleteCallback: ineffectualSingleDeleteCallback, + singleDeleteInvariantViolationCallback: singleDeleteInvariantViolationCallback, + formatVersion: formatVersion, } i.rangeDelFrag.Cmp = cmp i.rangeDelFrag.Format = formatKey @@ -306,21 +339,43 @@ func (i *compactionIter) Next() (*InternalKey, []byte) { return nil, nil } - // Prior to this call to `Next()` we are in one of three situations with + // Prior to this call to `Next()` we are in one of four situations with // respect to `iterKey` and related state: // // - `!skip && pos == iterPosNext`: `iterKey` is already at the next key. - // - `!skip && pos == iterPosCur`: We are at the key that has been returned. + // - `!skip && pos == iterPosCurForward`: We are at the key that has been returned. // To move forward we advance by one key, even if that lands us in the same // snapshot stripe. - // - `skip && pos == iterPosCur`: We are at the key that has been returned. + // - `skip && pos == iterPosCurForward`: We are at the key that has been returned. // To move forward we skip skippable entries in the stripe. + // - `skip && pos == iterPosNext && i.iterStripeChange == sameStripeNonSkippable`: + // This case may occur when skipping within a snapshot stripe and we + // encounter either: + // a) an invalid key kind; The previous call will have returned + // whatever key it was processing and deferred handling of the + // invalid key to this invocation of Next(). We're responsible for + // ignoring skip=true and falling into the invalid key kind case + // down below. + // b) an interleaved range delete; This is a wart of the current code + // structure. While skipping within a snapshot stripe, a range + // delete interleaved at its start key and sequence number + // interrupts the sequence of point keys. After we return the range + // delete to the caller, we need to pick up skipping at where we + // left off, so we preserve skip=true. + // TODO(jackson): This last case is confusing and can be removed if we + // interleave range deletions at the maximal sequence number using the + // keyspan interleaving iterator. This is the treatment given to range + // keys today. if i.pos == iterPosCurForward { if i.skip { i.skipInStripe() } else { i.nextInStripe() } + } else if i.skip { + if i.iterStripeChange != sameStripeNonSkippable { + panic(errors.AssertionFailedf("compaction iterator has skip=true, but iterator is at iterPosNext")) + } } i.pos = iterPosCurForward @@ -374,6 +429,10 @@ func (i *compactionIter) Next() (*InternalKey, []byte) { return &i.key, i.value } + // TODO(sumeer): we could avoid calling Covers if i.iterStripeChange == + // sameStripeSameKey since that check has already been done in + // nextInStripeHelper. However, we also need to handle the case of + // CoversInvisibly below. if cover := i.rangeDelFrag.Covers(*i.iterKey, i.curSnapshotSeqNum); cover == keyspan.CoversVisibly { // A pending range deletion deletes this key. Skip it. i.saveKey() @@ -382,17 +441,37 @@ func (i *compactionIter) Next() (*InternalKey, []byte) { } else if cover == keyspan.CoversInvisibly { // i.iterKey would be deleted by a range deletion if there weren't // any open snapshots. Mark it as pinned. + // + // NB: there are multiple places in this file where we call + // i.rangeDelFrag.Covers and this is the only one where we are writing + // to i.snapshotPinned. Those other cases occur in mergeNext where the + // caller is deciding whether the value should be merged or not, and the + // key is in the same snapshot stripe. Hence, snapshotPinned is by + // definition false in those cases. i.snapshotPinned = true + i.forceObsoleteDueToRangeDel = true + } else { + i.forceObsoleteDueToRangeDel = false } switch i.iterKey.Kind() { - case InternalKeyKindDelete, InternalKeyKindSingleDelete: + case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: if i.elideTombstone(i.iterKey.UserKey) { if i.curSnapshotIdx == 0 { // If we're at the last snapshot stripe and the tombstone // can be elided skip skippable keys in the same stripe. i.saveKey() - i.skipInStripe() + if i.key.Kind() == InternalKeyKindSingleDelete { + i.skipDueToSingleDeleteElision() + } else { + i.skipInStripe() + if !i.skip && i.iterStripeChange != newStripeNewKey { + panic(errors.AssertionFailedf("pebble: skipInStripe in last stripe disabled skip without advancing to new key")) + } + } + if i.iterStripeChange == newStripeSameKey { + panic(errors.AssertionFailedf("pebble: skipInStripe in last stripe found a new stripe within the same key")) + } continue } else { // We're not at the last snapshot stripe, so the tombstone @@ -410,11 +489,23 @@ func (i *compactionIter) Next() (*InternalKey, []byte) { i.skip = true return &i.key, i.value + case InternalKeyKindDeleteSized: + // We may skip subsequent keys because of this tombstone. Scan + // ahead to see just how much data this tombstone drops and if + // the tombstone's value should be updated accordingly. + return i.deleteSizedNext() + case InternalKeyKindSingleDelete: if i.singleDeleteNext() { return &i.key, i.value + } else if i.err != nil { + return nil, nil } continue + + default: + panic(errors.AssertionFailedf( + "unexpected kind %s", redact.SafeString(i.iterKey.Kind().String()))) } case InternalKeyKindSet, InternalKeyKindSetWithDelete: @@ -424,6 +515,9 @@ func (i *compactionIter) Next() (*InternalKey, []byte) { // preserving the original value, and potentially mutating the key // kind. i.setNext() + if i.err != nil { + return nil, nil + } return &i.key, i.value case InternalKeyKindMerge: @@ -440,7 +534,15 @@ func (i *compactionIter) Next() (*InternalKey, []byte) { if i.err == nil { // includesBase is true whenever we've transformed the MERGE record // into a SET. - includesBase := i.key.Kind() == InternalKeyKindSet + var includesBase bool + switch i.key.Kind() { + case InternalKeyKindSet, InternalKeyKindSetWithDelete: + includesBase = true + case InternalKeyKindMerge: + default: + panic(errors.AssertionFailedf( + "unexpected kind %s", redact.SafeString(i.key.Kind().String()))) + } i.value, needDelete, i.valueCloser, i.err = finishValueMerger(valueMerger, includesBase) } if i.err == nil { @@ -469,6 +571,8 @@ func (i *compactionIter) Next() (*InternalKey, []byte) { } if i.err != nil { i.valid = false + // TODO(sumeer): why is MarkCorruptionError only being called for + // MERGE? i.err = base.MarkCorruptionError(i.err) } return nil, nil @@ -508,10 +612,16 @@ func snapshotIndex(seq uint64, snapshots []uint64) (int, uint64) { return index, snapshots[index] } -// skipInStripe skips over skippable keys in the same stripe and user key. +// skipInStripe skips over skippable keys in the same stripe and user key. It +// may set i.err, in which case i.iterKey will be nil. func (i *compactionIter) skipInStripe() { i.skip = true + // TODO(sumeer): we can avoid the overhead of calling i.rangeDelFrag.Covers, + // in this case of nextInStripe, since we are skipping all of them anyway. for i.nextInStripe() == sameStripeSkippable { + if i.err != nil { + panic(i.err) + } } // Reset skip if we landed outside the original stripe. Otherwise, we landed // in the same stripe on a non-skippable key. In that case we should preserve @@ -549,11 +659,17 @@ const ( // nextInStripe advances the iterator and returns one of the above const ints // indicating how its state changed. // +// All sameStripeSkippable keys that are covered by a RANGEDEL will be skipped +// and not returned. +// // Calls to nextInStripe must be preceded by a call to saveKey to retain a // temporary reference to the original key, so that forward iteration can // proceed with a reference to the original key. Care should be taken to avoid // overwriting or mutating the saved key or value before they have been returned // to the caller of the exported function (i.e. the caller of Next, First, etc.) +// +// nextInStripe may set i.err, in which case the return value will be +// newStripeNewKey, and i.iterKey will be nil. func (i *compactionIter) nextInStripe() stripeChangeType { i.iterStripeChange = i.nextInStripeHelper() return i.iterStripeChange @@ -562,58 +678,72 @@ func (i *compactionIter) nextInStripe() stripeChangeType { // nextInStripeHelper is an internal helper for nextInStripe; callers should use // nextInStripe and not call nextInStripeHelper. func (i *compactionIter) nextInStripeHelper() stripeChangeType { - if !i.iterNext() { - return newStripeNewKey - } - key := i.iterKey + origSnapshotIdx := i.curSnapshotIdx + for { + if !i.iterNext() { + return newStripeNewKey + } + key := i.iterKey - // NB: The below conditional is an optimization to avoid a user key - // comparison in many cases. Internal keys with the same user key are - // ordered in (strictly) descending order by trailer. If the new key has a - // greater or equal trailer, or the previous key had a zero sequence number, - // the new key must have a new user key. - // - // A couple things make these cases common: - // - Sequence-number zeroing ensures ~all of the keys in L6 have a zero - // sequence number. - // - Ingested sstables' keys all adopt the same sequence number. - if i.keyTrailer <= base.InternalKeyZeroSeqnumMaxTrailer || key.Trailer >= i.keyTrailer { - if invariants.Enabled && i.equal(i.key.UserKey, key.UserKey) { + if !i.equal(i.key.UserKey, key.UserKey) { + i.curSnapshotIdx, i.curSnapshotSeqNum = snapshotIndex(key.SeqNum(), i.snapshots) + return newStripeNewKey + } + + // If i.key and key have the same user key, then + // 1. i.key must not have had a zero sequence number (or it would've be the last + // key with its user key). + // 2. i.key must have a strictly larger sequence number + // There's an exception in that either key may be a range delete. Range + // deletes may share a sequence number with a point key if the keys were + // ingested together. Range keys may also share the sequence number if they + // were ingested, but range keys are interleaved into the compaction + // iterator's input iterator at the maximal sequence number so their + // original sequence number will not be observed here. + if prevSeqNum := base.SeqNumFromTrailer(i.keyTrailer); (prevSeqNum == 0 || prevSeqNum <= key.SeqNum()) && + i.key.Kind() != InternalKeyKindRangeDelete && key.Kind() != InternalKeyKindRangeDelete { prevKey := i.key prevKey.Trailer = i.keyTrailer - panic(fmt.Sprintf("pebble: invariant violation: %s and %s out of order", key, prevKey)) + panic(errors.AssertionFailedf("pebble: invariant violation: %s and %s out of order", prevKey, key)) } + i.curSnapshotIdx, i.curSnapshotSeqNum = snapshotIndex(key.SeqNum(), i.snapshots) - return newStripeNewKey - } else if !i.equal(i.key.UserKey, key.UserKey) { - i.curSnapshotIdx, i.curSnapshotSeqNum = snapshotIndex(key.SeqNum(), i.snapshots) - return newStripeNewKey - } - origSnapshotIdx := i.curSnapshotIdx - i.curSnapshotIdx, i.curSnapshotSeqNum = snapshotIndex(key.SeqNum(), i.snapshots) - switch key.Kind() { - case InternalKeyKindRangeDelete: - // Range tombstones need to be exposed by the compactionIter to the upper level - // `compaction` object, so return them regardless of whether they are in the same - // snapshot stripe. - if i.curSnapshotIdx == origSnapshotIdx { - return sameStripeNonSkippable + switch key.Kind() { + case InternalKeyKindRangeDelete: + // Range tombstones need to be exposed by the compactionIter to the upper level + // `compaction` object, so return them regardless of whether they are in the same + // snapshot stripe. + if i.curSnapshotIdx == origSnapshotIdx { + return sameStripeNonSkippable + } + return newStripeSameKey + case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + // Range keys are interleaved at the max sequence number for a given user + // key, so we should not see any more range keys in this stripe. + panic("unreachable") + case InternalKeyKindInvalid: + if i.curSnapshotIdx == origSnapshotIdx { + return sameStripeNonSkippable + } + return newStripeSameKey + case InternalKeyKindDelete, InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindSingleDelete, + InternalKeyKindSetWithDelete, InternalKeyKindDeleteSized: + // Fall through + default: + i.iterKey = nil + i.err = base.CorruptionErrorf("invalid internal key kind: %d", errors.Safe(i.iterKey.Kind())) + i.valid = false + return newStripeNewKey } - return newStripeSameKey - case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: - // Range keys are interleaved at the max sequence number for a given user - // key, so we should not see any more range keys in this stripe. - panic("unreachable") - case InternalKeyKindInvalid: if i.curSnapshotIdx == origSnapshotIdx { - return sameStripeNonSkippable + // Same snapshot. + if i.rangeDelFrag.Covers(*i.iterKey, i.curSnapshotSeqNum) == keyspan.CoversVisibly { + continue + } + return sameStripeSkippable } return newStripeSameKey } - if i.curSnapshotIdx == origSnapshotIdx { - return sameStripeSkippable - } - return newStripeSameKey } func (i *compactionIter) setNext() { @@ -639,6 +769,10 @@ func (i *compactionIter) setNext() { // Else, we continue to loop through entries in the stripe looking for a // DEL. Note that we may stop *before* encountering a DEL, if one exists. + // + // NB: nextInStripe will skip sameStripeSkippable keys that are visibly + // covered by a RANGEDEL. This can include DELs -- this is fine since such + // DELs don't need to be combined with SET to make SETWITHDEL. for { switch i.nextInStripe() { case newStripeNewKey, newStripeSameKey: @@ -686,14 +820,19 @@ func (i *compactionIter) setNext() { i.skip = true return case sameStripeSkippable: - // We're still in the same stripe. If this is a DEL/SINGLEDEL, we - // stop looking and emit a SETWITHDEL. Subsequent keys are - // eligible for skipping. - if i.iterKey.Kind() == InternalKeyKindDelete || - i.iterKey.Kind() == InternalKeyKindSingleDelete { + // We're still in the same stripe. If this is a + // DEL/SINGLEDEL/DELSIZED, we stop looking and emit a SETWITHDEL. + // Subsequent keys are eligible for skipping. + switch i.iterKey.Kind() { + case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: i.key.SetKind(InternalKeyKindSetWithDelete) i.skip = true return + case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindSetWithDelete: + // Do nothing + default: + i.err = base.CorruptionErrorf("invalid internal key kind: %d", errors.Safe(i.iterKey.Kind())) + i.valid = false } default: panic("pebble: unexpected stripeChangeType: " + strconv.Itoa(int(i.iterStripeChange))) @@ -713,13 +852,24 @@ func (i *compactionIter) mergeNext(valueMerger ValueMerger) stripeChangeType { i.pos = iterPosNext return i.iterStripeChange } + if i.err != nil { + panic(i.err) + } + // NB: MERGE#10+RANGEDEL#9 stays a MERGE, since nextInStripe skips + // sameStripeSkippable keys that are visibly covered by a RANGEDEL. There + // may be MERGE#7 that is invisibly covered and will be preserved, but + // there is no risk that MERGE#10 and MERGE#7 will get merged in the + // future as the RANGEDEL still exists and will be used in user-facing + // reads that see MERGE#10, and will also eventually cause MERGE#7 to be + // deleted in a compaction. key := i.iterKey switch key.Kind() { - case InternalKeyKindDelete, InternalKeyKindSingleDelete: + case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: // We've hit a deletion tombstone. Return everything up to this point and // then skip entries until the next snapshot stripe. We change the kind // of the result key to a Set so that it shadows keys in lower - // levels. That is, MERGE+DEL -> SET. + // levels. That is, MERGE+DEL -> SETWITHDEL. + // // We do the same for SingleDelete since SingleDelete is only // permitted (with deterministic behavior) for keys that have been // set once since the last SingleDelete/Delete, so everything @@ -732,21 +882,11 @@ func (i *compactionIter) mergeNext(valueMerger ValueMerger) stripeChangeType { // single Set, and then merge in any following Sets, but that is // complicated wrt code and unnecessary given the narrow permitted // use of SingleDelete. - i.key.SetKind(InternalKeyKindSet) + i.key.SetKind(InternalKeyKindSetWithDelete) i.skip = true return sameStripeSkippable case InternalKeyKindSet, InternalKeyKindSetWithDelete: - if i.rangeDelFrag.Covers(*key, i.curSnapshotSeqNum) == keyspan.CoversVisibly { - // We change the kind of the result key to a Set so that it shadows - // keys in lower levels. That is, MERGE+RANGEDEL -> SET. This isn't - // strictly necessary, but provides consistency with the behavior of - // MERGE+DEL. - i.key.SetKind(InternalKeyKindSet) - i.skip = true - return sameStripeSkippable - } - // We've hit a Set or SetWithDel value. Merge with the existing // value and return. We change the kind of the resulting key to a // Set so that it shadows keys in lower levels. That is: @@ -761,16 +901,6 @@ func (i *compactionIter) mergeNext(valueMerger ValueMerger) stripeChangeType { return sameStripeSkippable case InternalKeyKindMerge: - if i.rangeDelFrag.Covers(*key, i.curSnapshotSeqNum) == keyspan.CoversVisibly { - // We change the kind of the result key to a Set so that it shadows - // keys in lower levels. That is, MERGE+RANGEDEL -> SET. This isn't - // strictly necessary, but provides consistency with the behavior of - // MERGE+DEL. - i.key.SetKind(InternalKeyKindSet) - i.skip = true - return sameStripeSkippable - } - // We've hit another Merge value. Merge with the existing value and // continue looping. i.err = valueMerger.MergeOlder(i.iterValue) @@ -787,6 +917,16 @@ func (i *compactionIter) mergeNext(valueMerger ValueMerger) stripeChangeType { } } +// singleDeleteNext processes a SingleDelete point tombstone. A SingleDelete, or +// SINGLEDEL, is unique in that it deletes exactly 1 internal key. It's a +// performance optimization when the client knows a user key has not been +// overwritten, allowing the elision of the tombstone earlier, avoiding write +// amplification. +// +// singleDeleteNext returns a boolean indicating whether or not the caller +// should yield the SingleDelete key to the consumer of the compactionIter. If +// singleDeleteNext returns false, the caller may consume/elide the +// SingleDelete. func (i *compactionIter) singleDeleteNext() bool { // Save the current key. i.saveKey() @@ -795,26 +935,85 @@ func (i *compactionIter) singleDeleteNext() bool { // Loop until finds a key to be passed to the next level. for { + // If we find a key that can't be skipped, return true so that the + // caller yields the SingleDelete to the caller. if i.nextInStripe() != sameStripeSkippable { + // This defers additional error checking regarding single delete + // invariants to the compaction where the keys with the same user key as + // the single delete are in the same stripe. i.pos = iterPosNext - return true + return i.err == nil } - + if i.err != nil { + panic(i.err) + } + // INVARIANT: sameStripeSkippable. key := i.iterKey - switch key.Kind() { - case InternalKeyKindDelete, InternalKeyKindMerge, InternalKeyKindSetWithDelete: - // We've hit a Delete, Merge or SetWithDelete, transform the - // SingleDelete into a full Delete. + kind := key.Kind() + switch kind { + case InternalKeyKindDelete, InternalKeyKindSetWithDelete, InternalKeyKindDeleteSized: + if (kind == InternalKeyKindDelete || kind == InternalKeyKindDeleteSized) && + i.ineffectualSingleDeleteCallback != nil { + i.ineffectualSingleDeleteCallback(i.key.UserKey) + } + // We've hit a Delete, DeleteSized, SetWithDelete, transform + // the SingleDelete into a full Delete. i.key.SetKind(InternalKeyKindDelete) i.skip = true return true - case InternalKeyKindSet: - i.nextInStripe() + case InternalKeyKindSet, InternalKeyKindMerge: + // This SingleDelete deletes the Set/Merge, and we can now elide the + // SingleDel as well. We advance past the Set and return false to + // indicate to the main compaction loop that we should NOT yield the + // current SingleDel key to the compaction loop. + // + // NB: singleDeleteNext was called with i.pos == iterPosCurForward, and + // after the call to nextInStripe, we are still at iterPosCurForward, + // since we are at the key after the Set/Merge that was single deleted. + change := i.nextInStripe() + switch change { + case sameStripeSkippable, newStripeSameKey: + // On the same user key. + nextKind := i.iterKey.Kind() + switch nextKind { + case InternalKeyKindSet, InternalKeyKindSetWithDelete, InternalKeyKindMerge: + if i.singleDeleteInvariantViolationCallback != nil { + // sameStripeSkippable keys returned by nextInStripe() are already + // known to not be covered by a RANGEDEL, so it is an invariant + // violation. The rare case is newStripeSameKey, where it is a + // violation if not covered by a RANGEDEL. + if change == sameStripeSkippable || + i.rangeDelFrag.Covers(*i.iterKey, i.curSnapshotSeqNum) == keyspan.NoCover { + i.singleDeleteInvariantViolationCallback(i.key.UserKey) + } + } + case InternalKeyKindDelete, InternalKeyKindDeleteSized, InternalKeyKindSingleDelete, + InternalKeyKindRangeDelete: + default: + panic(errors.AssertionFailedf( + "unexpected internal key kind: %d", errors.Safe(i.iterKey.Kind()))) + } + case sameStripeNonSkippable: + // No ability to check whether there is another Set/Merge below with + // the same user key. + // + // TODO(sumeer): once range deletions are interleaved at the maximal + // sequence number, this case will go away. + case newStripeNewKey: + default: + panic("unreachable") + } i.valid = false return false case InternalKeyKindSingleDelete: + // Two single deletes met in a compaction. The first single delete is + // ineffectual. + if i.ineffectualSingleDeleteCallback != nil { + i.ineffectualSingleDeleteCallback(i.key.UserKey) + } + // Continue to apply the second single delete. continue default: @@ -825,6 +1024,303 @@ func (i *compactionIter) singleDeleteNext() bool { } } +// skipDueToSingleDeleteElision is called when the SingleDelete is being +// elided because it is in the final snapshot stripe and there are no keys +// with the same user key in lower levels in the LSM (below the files in this +// compaction). +// +// TODO(sumeer): the only difference between singleDeleteNext and +// skipDueToSingleDeleteElision is the fact that the caller knows it will be +// eliding the single delete in the latter case. There are some similar things +// happening in both implementations. My first attempt at combining them into +// a single method was hard to comprehend. Try again. +func (i *compactionIter) skipDueToSingleDeleteElision() { + for { + stripeChange := i.nextInStripe() + if i.err != nil { + panic(i.err) + } + switch stripeChange { + case newStripeNewKey: + // The single delete is only now being elided, meaning it did not elide + // any keys earlier in its descent down the LSM. We stepped onto a new + // user key, meaning that even now at its moment of elision, it still + // hasn't elided any other keys. The single delete was ineffectual (a + // no-op). + if i.ineffectualSingleDeleteCallback != nil { + i.ineffectualSingleDeleteCallback(i.key.UserKey) + } + i.skip = false + return + case newStripeSameKey: + // This should be impossible. If we're eliding a single delete, we + // determined that the tombstone is in the final snapshot stripe, but we + // stepped into a new stripe of the same key. + panic(errors.AssertionFailedf("eliding single delete followed by same key in new stripe")) + case sameStripeNonSkippable: + // There's a key that we cannot skip. There are two possible cases: + // a. The key is invalid. This is an error. + // b. The key is a range deletion. + // The second case may also be an ineffectual single delete. However, it + // is possible that there is a SET that is at the same seqnum as the + // RANGEDEL, and so is not deleted by that RANGEDEL, and will be deleted + // by this single delete. So we cannot be certain that this is an + // ineffectual single delete. + // + // TODO(sumeer): the existing todo to interleave range deletions at the + // maximal sequence number will allow us to address this ambiguity. + // + // TODO(sumeer): by setting skip to true, the compactionIter is making a + // single delete stronger (like a del), which will hide bugs in the use of + // single delete. + i.skip = true + return + case sameStripeSkippable: + kind := i.iterKey.Kind() + switch kind { + case InternalKeyKindDelete, InternalKeyKindDeleteSized, InternalKeyKindSingleDelete: + if i.ineffectualSingleDeleteCallback != nil { + i.ineffectualSingleDeleteCallback(i.key.UserKey) + } + switch kind { + case InternalKeyKindDelete, InternalKeyKindDeleteSized: + i.skipInStripe() + return + case InternalKeyKindSingleDelete: + // Repeat the same with this SingleDelete. We don't want to simply + // call skipInStripe(), since it increases the strength of the + // SingleDel, which hides bugs in the use of single delete. + continue + default: + panic(errors.AssertionFailedf( + "unexpected internal key kind: %d", errors.Safe(i.iterKey.Kind()))) + } + case InternalKeyKindSetWithDelete: + // The SingleDelete should behave like a Delete. + i.skipInStripe() + return + case InternalKeyKindSet, InternalKeyKindMerge: + // This SingleDelete deletes the Set/Merge, and we are eliding the + // SingleDel as well. Step to the next key (this is not deleted by the + // SingleDelete). + // + // NB: skipDueToSingleDeleteElision was called with i.pos == + // iterPosCurForward, and after the call to nextInStripe, we are still + // at iterPosCurForward, since we are at the key after the Set/Merge + // that was single deleted. + change := i.nextInStripe() + if i.err != nil { + panic(i.err) + } + switch change { + case newStripeSameKey: + panic(errors.AssertionFailedf("eliding single delete followed by same key in new stripe")) + case newStripeNewKey: + case sameStripeSkippable: + // On the same key. + nextKind := i.iterKey.Kind() + switch nextKind { + case InternalKeyKindSet, InternalKeyKindSetWithDelete, InternalKeyKindMerge: + if i.singleDeleteInvariantViolationCallback != nil { + i.singleDeleteInvariantViolationCallback(i.key.UserKey) + } + case InternalKeyKindDelete, InternalKeyKindDeleteSized, InternalKeyKindSingleDelete, + InternalKeyKindRangeDelete: + default: + panic(errors.AssertionFailedf( + "unexpected internal key kind: %d", errors.Safe(i.iterKey.Kind()))) + } + case sameStripeNonSkippable: + // No ability to check whether there is another Set/Merge below with + // the same user key. + // + // TODO(sumeer): once range deletions are interleaved at the maximal + // sequence number, this case will go away. + default: + panic("unreachable") + } + // Whether in same stripe or new stripe, this key is not consumed by + // the SingleDelete. + i.skip = false + return + default: + panic(errors.AssertionFailedf( + "unexpected internal key kind: %d", errors.Safe(i.iterKey.Kind()))) + } + default: + panic("unreachable") + } + } +} + +// deleteSizedNext processes a DELSIZED point tombstone. Unlike ordinary DELs, +// these tombstones carry a value that's a varint indicating the size of the +// entry (len(key)+len(value)) that the tombstone is expected to delete. +// +// When a deleteSizedNext is encountered, we skip ahead to see which keys, if +// any, are elided as a result of the tombstone. +func (i *compactionIter) deleteSizedNext() (*base.InternalKey, []byte) { + i.saveKey() + i.valid = true + i.skip = true + + // The DELSIZED tombstone may have no value at all. This happens when the + // tombstone has already deleted the key that the user originally predicted. + // In this case, we still peek forward in case there's another DELSIZED key + // with a lower sequence number, in which case we'll adopt its value. + if len(i.iterValue) == 0 { + i.value = i.valueBuf[:0] + } else { + i.valueBuf = append(i.valueBuf[:0], i.iterValue...) + i.value = i.valueBuf + } + + // Loop through all the keys within this stripe that are skippable. + i.pos = iterPosNext + for i.nextInStripe() == sameStripeSkippable { + if i.err != nil { + panic(i.err) + } + switch i.iterKey.Kind() { + case InternalKeyKindDelete, InternalKeyKindDeleteSized, InternalKeyKindSingleDelete: + // We encountered a tombstone (DEL, or DELSIZED) that's deleted by + // the original DELSIZED tombstone. This can happen in two cases: + // + // (1) These tombstones were intended to delete two distinct values, + // and this DELSIZED has already dropped the relevant key. For + // example: + // + // a.DELSIZED.9 a.SET.7 a.DELSIZED.5 a.SET.4 + // + // If a.DELSIZED.9 has already deleted a.SET.7, its size has + // already been zeroed out. In this case, we want to adopt the + // value of the DELSIZED with the lower sequence number, in + // case the a.SET.4 key has not yet been elided. + // + // (2) This DELSIZED was missized. The user thought they were + // deleting a key with this user key, but this user key had + // already been deleted. + // + // We can differentiate these two cases by examining the length of + // the DELSIZED's value. A DELSIZED's value holds the size of both + // the user key and value that it intends to delete. For any user + // key with a length > 0, a DELSIZED that has not deleted a key must + // have a value with a length > 0. + // + // We treat both cases the same functionally, adopting the identity + // of the lower-sequence numbered tombstone. However in the second + // case, we also increment the stat counting missized tombstones. + if len(i.value) > 0 { + // The original DELSIZED key was missized. The key that the user + // thought they were deleting does not exist. + i.stats.countMissizedDels++ + } + i.valueBuf = append(i.valueBuf[:0], i.iterValue...) + i.value = i.valueBuf + if i.iterKey.Kind() != InternalKeyKindDeleteSized { + // Convert the DELSIZED to a DEL—The DEL/SINGLEDEL we're eliding + // may not have deleted the key(s) it was intended to yet. The + // ordinary DEL compaction heuristics are better suited at that, + // plus we don't want to count it as a missized DEL. We early + // exit in this case, after skipping the remainder of the + // snapshot stripe. + i.key.SetKind(InternalKeyKindDelete) + // NB: We skipInStripe now, rather than returning leaving + // i.skip=true and returning early, because Next() requires + // that i.skip=true only if i.iterPos = iterPosCurForward. + // + // Ignore any error caused by skipInStripe since it does not affect + // the key/value being returned here, and the next call to Next() will + // expose it. + i.skipInStripe() + return &i.key, i.value + } + // Continue, in case we uncover another DELSIZED or a key this + // DELSIZED deletes. + + case InternalKeyKindSet, InternalKeyKindMerge, InternalKeyKindSetWithDelete: + // If the DELSIZED is value-less, it already deleted the key that it + // was intended to delete. This is possible with a sequence like: + // + // DELSIZED.8 SET.7 SET.3 + // + // The DELSIZED only describes the size of the SET.7, which in this + // case has already been elided. We don't count it as a missizing, + // instead converting the DELSIZED to a DEL. Skip the remainder of + // the snapshot stripe and return. + if len(i.value) == 0 { + i.key.SetKind(InternalKeyKindDelete) + // NB: We skipInStripe now, rather than returning leaving + // i.skip=true and returning early, because Next() requires + // that i.skip=true only if i.iterPos = iterPosCurForward. + // + // Ignore any error caused by skipInStripe since it does not affect + // the key/value being returned here, and the next call to Next() will + // expose it. + i.skipInStripe() + return &i.key, i.value + } + // The deleted key is not a DEL, DELSIZED, and the DELSIZED in i.key + // has a positive size. + expectedSize, n := binary.Uvarint(i.value) + if n != len(i.value) { + i.err = base.CorruptionErrorf("DELSIZED holds invalid value: %x", errors.Safe(i.value)) + i.valid = false + return nil, nil + } + elidedSize := uint64(len(i.iterKey.UserKey)) + uint64(len(i.iterValue)) + if elidedSize != expectedSize { + // The original DELSIZED key was missized. It's unclear what to + // do. The user-provided size was wrong, so it's unlikely to be + // accurate or meaningful. We could: + // + // 1. return the DELSIZED with the original user-provided size unmodified + // 2. return the DELZIZED with a zeroed size to reflect that a key was + // elided, even if it wasn't the anticipated size. + // 3. subtract the elided size from the estimate and re-encode. + // 4. convert the DELSIZED into a value-less DEL, so that + // ordinary DEL heuristics apply. + // + // We opt for (4) under the rationale that we can't rely on the + // user-provided size for accuracy, so ordinary DEL heuristics + // are safer. + i.stats.countMissizedDels++ + i.key.SetKind(InternalKeyKindDelete) + i.value = i.valueBuf[:0] + // NB: We skipInStripe now, rather than returning leaving + // i.skip=true and returning early, because Next() requires + // that i.skip=true only if i.iterPos = iterPosCurForward. + // + // Ignore any error caused by skipInStripe since it does not affect + // the key/value being returned here, and the next call to Next() will + // expose it. + i.skipInStripe() + return &i.key, i.value + } + // NB: We remove the value regardless of whether the key was sized + // appropriately. The size encoded is 'consumed' the first time it + // meets a key that it deletes. + i.value = i.valueBuf[:0] + + default: + i.err = base.CorruptionErrorf("invalid internal key kind: %d", errors.Safe(i.iterKey.Kind())) + i.valid = false + return nil, nil + } + } + // Reset skip if we landed outside the original stripe. Otherwise, we landed + // in the same stripe on a non-skippable key. In that case we should preserve + // `i.skip == true` such that later keys in the stripe will continue to be + // skipped. + if i.iterStripeChange == newStripeNewKey || i.iterStripeChange == newStripeSameKey { + i.skip = false + } + if i.err != nil { + return nil, nil + } + return &i.key, i.value +} + func (i *compactionIter) saveKey() { i.keyBuf = append(i.keyBuf[:0], i.iterKey.UserKey...) i.key.UserKey = i.keyBuf @@ -957,7 +1453,7 @@ func (i *compactionIter) maybeZeroSeqnum(snapshotIdx int) { // This is not the last snapshot return } - i.key.SetSeqNum(0) + i.key.SetSeqNum(base.SeqNumZero) } // A frontier is used to monitor a compaction's progression across the user diff --git a/vendor/github.com/cockroachdb/pebble/compaction_picker.go b/vendor/github.com/cockroachdb/pebble/compaction_picker.go index afd15b0..f35d475 100644 --- a/vendor/github.com/cockroachdb/pebble/compaction_picker.go +++ b/vendor/github.com/cockroachdb/pebble/compaction_picker.go @@ -21,6 +21,17 @@ import ( const minIntraL0Count = 4 type compactionEnv struct { + // diskAvailBytes holds a statistic on the number of bytes available on + // disk, as reported by the filesystem. It's used to be more restrictive in + // expanding compactions if available disk space is limited. + // + // The cached value (d.diskAvailBytes) is updated whenever a file is deleted + // and whenever a compaction or flush completes. Since file removal is the + // primary means of reclaiming space, there is a rough bound on the + // statistic's staleness when available bytes is growing. Compactions and + // flushes are longer, slower operations and provide a much looser bound + // when available bytes is decreasing. + diskAvailBytes uint64 earliestUnflushedSeqNum uint64 earliestSnapshotSeqNum uint64 inProgressCompactions []compactionInfo @@ -30,10 +41,8 @@ type compactionEnv struct { type compactionPicker interface { getScores([]compactionInfo) [numLevels]float64 getBaseLevel() int - getEstimatedMaxWAmp() float64 estimatedCompactionDebt(l0ExtraSize uint64) uint64 pickAuto(env compactionEnv) (pc *pickedCompaction) - pickManual(env compactionEnv, manual *manualCompaction) (c *pickedCompaction, retryLater bool) pickElisionOnlyCompaction(env compactionEnv) (pc *pickedCompaction) pickRewriteCompaction(env compactionEnv) (pc *pickedCompaction) pickReadTriggeredCompaction(env compactionEnv) (pc *pickedCompaction) @@ -47,13 +56,17 @@ type readCompactionEnv struct { flushing bool } -// Information about in-progress compactions provided to the compaction picker. These are used to -// constrain the new compactions that will be picked. +// Information about in-progress compactions provided to the compaction picker. +// These are used to constrain the new compactions that will be picked. type compactionInfo struct { - inputs []compactionLevel - outputLevel int - smallest InternalKey - largest InternalKey + // versionEditApplied is true if this compaction's version edit has already + // been committed. The compaction may still be in-progress deleting newly + // obsolete files. + versionEditApplied bool + inputs []compactionLevel + outputLevel int + smallest InternalKey + largest InternalKey } func (info compactionInfo) String() string { @@ -77,18 +90,38 @@ func (info compactionInfo) String() string { return buf.String() } -type sortCompactionLevelsDecreasingScore []candidateLevelInfo +type sortCompactionLevelsByPriority []candidateLevelInfo -func (s sortCompactionLevelsDecreasingScore) Len() int { +func (s sortCompactionLevelsByPriority) Len() int { return len(s) } -func (s sortCompactionLevelsDecreasingScore) Less(i, j int) bool { - if s[i].score != s[j].score { - return s[i].score > s[j].score + +// A level should be picked for compaction if the compensatedScoreRatio is >= the +// compactionScoreThreshold. +const compactionScoreThreshold = 1 + +// Less should return true if s[i] must be placed earlier than s[j] in the final +// sorted list. The candidateLevelInfo for the level placed earlier is more likely +// to be picked for a compaction. +func (s sortCompactionLevelsByPriority) Less(i, j int) bool { + iShouldCompact := s[i].compensatedScoreRatio >= compactionScoreThreshold + jShouldCompact := s[j].compensatedScoreRatio >= compactionScoreThreshold + // Ordering is defined as decreasing on (shouldCompact, uncompensatedScoreRatio) + // where shouldCompact is 1 for true and 0 for false. + if iShouldCompact && !jShouldCompact { + return true + } + if !iShouldCompact && jShouldCompact { + return false + } + + if s[i].uncompensatedScoreRatio != s[j].uncompensatedScoreRatio { + return s[i].uncompensatedScoreRatio > s[j].uncompensatedScoreRatio } return s[i].level < s[j].level } -func (s sortCompactionLevelsDecreasingScore) Swap(i, j int) { + +func (s sortCompactionLevelsByPriority) Swap(i, j int) { s[i], s[j] = s[j], s[i] } @@ -138,49 +171,43 @@ func generateSublevelInfo(cmp base.Compare, levelFiles manifest.LevelSlice) []su return levelSlices } +// compactionPickerMetrics holds metrics related to the compaction picking process +type compactionPickerMetrics struct { + // scores contains the compensatedScoreRatio from the candidateLevelInfo. + scores []float64 + singleLevelOverlappingRatio float64 + multiLevelOverlappingRatio float64 +} + // pickedCompaction contains information about a compaction that has already // been chosen, and is being constructed. Compaction construction info lives in // this struct, and is copied over into the compaction struct when that's // created. type pickedCompaction struct { cmp Compare - - // score of the chosen compaction. Taken from candidateLevelInfo. + // score of the chosen compaction. This is the same as the + // compensatedScoreRatio in the candidateLevelInfo. score float64 - // kind indicates the kind of compaction. kind compactionKind - // startLevel is the level that is being compacted. Inputs from startLevel // and outputLevel will be merged to produce a set of outputLevel files. startLevel *compactionLevel - // outputLevel is the level that files are being produced in. outputLevel is // equal to startLevel+1 except when: // - if startLevel is 0, the output level equals compactionPicker.baseLevel(). // - in multilevel compaction, the output level is the lowest level involved in // the compaction outputLevel *compactionLevel - // extraLevels contain additional levels in between the input and output // levels that get compacted in multi level compactions extraLevels []*compactionLevel - - // adjustedOutputLevel is the output level used for the purpose of - // determining the target output file size, overlap bytes, and expanded - // bytes, taking into account the base level. - adjustedOutputLevel int - - inputs []compactionLevel - + inputs []compactionLevel + // LBase at the time of compaction picking. + baseLevel int // L0-specific compaction info. Set to a non-nil value for all compactions // where startLevel == 0 that were generated by L0Sublevels. lcf *manifest.L0CompactionFiles - - // L0SublevelInfo is used for compactions out of L0. It is nil for all - // other compactions. - l0SublevelInfo []sublevelInfo - // maxOutputFileSize is the maximum size of an individual table created // during compaction. maxOutputFileSize uint64 @@ -191,12 +218,11 @@ type pickedCompaction struct { // overlap in its output level with. If the overlap is greater than // maxReadCompaction bytes, then we don't proceed with the compaction. maxReadCompactionBytes uint64 - // The boundaries of the input data. - smallest InternalKey - largest InternalKey - - version *version + smallest InternalKey + largest InternalKey + version *version + pickerMetrics compactionPickerMetrics } func defaultOutputLevel(startLevel, baseLevel int) int { @@ -218,28 +244,35 @@ func newPickedCompaction( startLevel, baseLevel)) } - adjustedOutputLevel := outputLevel - if adjustedOutputLevel > 0 { - // Output level is in the range [baseLevel,numLevels]. For the purpose of - // determining the target output file size, overlap bytes, and expanded - // bytes, we want to adjust the range to [1,numLevels]. - adjustedOutputLevel = 1 + outputLevel - baseLevel - } - + adjustedLevel := adjustedOutputLevel(outputLevel, baseLevel) pc := &pickedCompaction{ cmp: opts.Comparer.Compare, version: cur, + baseLevel: baseLevel, inputs: []compactionLevel{{level: startLevel}, {level: outputLevel}}, - adjustedOutputLevel: adjustedOutputLevel, - maxOutputFileSize: uint64(opts.Level(adjustedOutputLevel).TargetFileSize), - maxOverlapBytes: maxGrandparentOverlapBytes(opts, adjustedOutputLevel), - maxReadCompactionBytes: maxReadCompactionBytes(opts, adjustedOutputLevel), + maxOutputFileSize: uint64(opts.Level(adjustedLevel).TargetFileSize), + maxOverlapBytes: maxGrandparentOverlapBytes(opts, adjustedLevel), + maxReadCompactionBytes: maxReadCompactionBytes(opts, adjustedLevel), } pc.startLevel = &pc.inputs[0] pc.outputLevel = &pc.inputs[1] return pc } +// adjustedOutputLevel is the output level used for the purpose of +// determining the target output file size, overlap bytes, and expanded +// bytes, taking into account the base level. +func adjustedOutputLevel(outputLevel int, baseLevel int) int { + adjustedOutputLevel := outputLevel + if adjustedOutputLevel > 0 { + // Output level is in the range [baseLevel, numLevels]. For the purpose of + // determining the target output file size, overlap bytes, and expanded + // bytes, we want to adjust the range to [1,numLevels]. + adjustedOutputLevel = 1 + outputLevel - baseLevel + } + return adjustedOutputLevel +} + func newPickedCompactionFromL0( lcf *manifest.L0CompactionFiles, opts *Options, vers *version, baseLevel int, isBase bool, ) *pickedCompaction { @@ -272,7 +305,7 @@ func (pc *pickedCompaction) String() string { var builder strings.Builder builder.WriteString(fmt.Sprintf(`Score=%f, `, pc.score)) builder.WriteString(fmt.Sprintf(`Kind=%s, `, pc.kind)) - builder.WriteString(fmt.Sprintf(`AdjustedOutputLevel=%d, `, pc.adjustedOutputLevel)) + builder.WriteString(fmt.Sprintf(`AdjustedOutputLevel=%d, `, adjustedOutputLevel(pc.outputLevel.level, pc.baseLevel))) builder.WriteString(fmt.Sprintf(`maxOutputFileSize=%d, `, pc.maxOutputFileSize)) builder.WriteString(fmt.Sprintf(`maxReadCompactionBytes=%d, `, pc.maxReadCompactionBytes)) builder.WriteString(fmt.Sprintf(`smallest=%s, `, pc.smallest)) @@ -282,7 +315,7 @@ func (pc *pickedCompaction) String() string { builder.WriteString(fmt.Sprintf(`startlevel=%s, `, pc.startLevel)) builder.WriteString(fmt.Sprintf(`outputLevel=%s, `, pc.outputLevel)) builder.WriteString(fmt.Sprintf(`extraLevels=%s, `, pc.extraLevels)) - builder.WriteString(fmt.Sprintf(`l0SublevelInfo=%s, `, pc.l0SublevelInfo)) + builder.WriteString(fmt.Sprintf(`l0SublevelInfo=%s, `, pc.startLevel.l0SublevelInfo)) builder.WriteString(fmt.Sprintf(`lcf=%s`, pc.lcf)) return builder.String() } @@ -296,13 +329,16 @@ func (pc *pickedCompaction) clone() *pickedCompaction { cmp: pc.cmp, score: pc.score, kind: pc.kind, - adjustedOutputLevel: pc.adjustedOutputLevel, + baseLevel: pc.baseLevel, maxOutputFileSize: pc.maxOutputFileSize, maxOverlapBytes: pc.maxOverlapBytes, maxReadCompactionBytes: pc.maxReadCompactionBytes, smallest: pc.smallest.Clone(), largest: pc.largest.Clone(), + // TODO(msbutler): properly clone picker metrics + pickerMetrics: pc.pickerMetrics, + // Both copies see the same manifest, therefore, it's ok for them to se // share the same pc. version. version: pc.version, @@ -321,9 +357,11 @@ func (pc *pickedCompaction) clone() *pickedCompaction { } } - newPC.l0SublevelInfo = make([]sublevelInfo, len(pc.l0SublevelInfo)) - for i := range pc.l0SublevelInfo { - newPC.l0SublevelInfo[i] = pc.l0SublevelInfo[i].Clone() + if len(pc.startLevel.l0SublevelInfo) > 0 { + newPC.startLevel.l0SublevelInfo = make([]sublevelInfo, len(pc.startLevel.l0SublevelInfo)) + for i := range pc.startLevel.l0SublevelInfo { + newPC.startLevel.l0SublevelInfo[i] = pc.startLevel.l0SublevelInfo[i].Clone() + } } if pc.lcf != nil { newPC.lcf = pc.lcf.Clone() @@ -369,7 +407,7 @@ func (pc *pickedCompaction) setupInputs( // growing a compaction results in a larger size, the original compaction // is used instead. maxExpandedBytes := expandedCompactionByteSizeLimit( - opts, pc.adjustedOutputLevel, diskAvailBytes, + opts, adjustedOutputLevel(pc.outputLevel.level, pc.baseLevel), diskAvailBytes, ) // Expand the initial inputs to a clean cut. @@ -464,7 +502,7 @@ func (pc *pickedCompaction) setupInputs( if pc.startLevel.level == 0 { // We don't change the input files for the compaction beyond this point. - pc.l0SublevelInfo = generateSublevelInfo(pc.cmp, pc.startLevel.files) + pc.startLevel.l0SublevelInfo = generateSublevelInfo(pc.cmp, pc.startLevel.files) } return true @@ -527,8 +565,6 @@ func (pc *pickedCompaction) setupMultiLevelCandidate(opts *Options, diskAvailByt pc.startLevel = &pc.inputs[0] pc.extraLevels = []*compactionLevel{&pc.inputs[1]} pc.outputLevel = &pc.inputs[2] - - pc.adjustedOutputLevel++ return pc.setupInputs(opts, diskAvailBytes, pc.extraLevels[len(pc.extraLevels)-1]) } @@ -639,17 +675,11 @@ func expandToAtomicUnit( } func newCompactionPicker( - v *version, - opts *Options, - inProgressCompactions []compactionInfo, - levelSizes [numLevels]int64, - diskAvailBytes func() uint64, + v *version, opts *Options, inProgressCompactions []compactionInfo, ) compactionPicker { p := &compactionPickerByScore{ - opts: opts, - vers: v, - levelSizes: levelSizes, - diskAvailBytes: diskAvailBytes, + opts: opts, + vers: v, } p.initLevelMaxBytes(inProgressCompactions) return p @@ -658,16 +688,23 @@ func newCompactionPicker( // Information about a candidate compaction level that has been identified by // the compaction picker. type candidateLevelInfo struct { - // The score of the level to be compacted, with compensated file sizes and - // adjustments. - score float64 - // The original score of the level to be compacted, before adjusting - // according to other levels' sizes. - origScore float64 - // The raw score of the level to be compacted, calculated using - // uncompensated file sizes and without any adjustments. - rawScore float64 - level int + // The compensatedScore of the level after adjusting according to the other + // levels' sizes. For L0, the compensatedScoreRatio is equivalent to the + // uncompensatedScoreRatio as we don't account for level size compensation in + // L0. + compensatedScoreRatio float64 + // The score of the level after accounting for level size compensation before + // adjusting according to other levels' sizes. For L0, the compensatedScore + // is equivalent to the uncompensatedScore as we don't account for level + // size compensation in L0. + compensatedScore float64 + // The score of the level to be compacted, calculated using uncompensated file + // sizes and without any adjustments. + uncompensatedScore float64 + // uncompensatedScoreRatio is the uncompensatedScore adjusted according to + // the other levels' sizes. + uncompensatedScoreRatio float64 + level int // The level to compact to. outputLevel int // The file in level that will be compacted. Additional files may be @@ -676,6 +713,10 @@ type candidateLevelInfo struct { file manifest.LevelFile } +func (c *candidateLevelInfo) shouldCompact() bool { + return c.compensatedScoreRatio >= compactionScoreThreshold +} + func fileCompensation(f *fileMetadata) uint64 { return uint64(f.Stats.PointDeletionsBytesEstimate) + f.Stats.RangeDeletionsBytesEstimate } @@ -712,7 +753,7 @@ func (a compensatedSizeAnnotator) Accumulate( ) (v interface{}, cacheOK bool) { vptr := dst.(*uint64) *vptr = *vptr + compensatedSize(f) - return vptr, f.StatsValidLocked() + return vptr, f.StatsValid() } func (a compensatedSizeAnnotator) Merge(src interface{}, dst interface{}) interface{} { @@ -740,42 +781,20 @@ func totalCompensatedSize(iter manifest.LevelIterator) uint64 { type compactionPickerByScore struct { opts *Options vers *version - // The level to target for L0 compactions. Levels L1 to baseLevel must be // empty. baseLevel int - - // estimatedMaxWAmp is the estimated maximum write amp per byte that is - // added to L0. - estimatedMaxWAmp float64 - // levelMaxBytes holds the dynamically adjusted max bytes setting for each // level. levelMaxBytes [numLevels]int64 - - // levelSizes holds the current size of each level. - levelSizes [numLevels]int64 - - // diskAvailBytes returns a cached statistic on the number of bytes - // available on disk, as reported by the filesystem. It's used to be more - // restrictive in expanding compactions if available disk space is - // limited. - // - // The cached value is updated whenever a file is deleted and - // whenever a compaction or flush completes. Since file removal is - // the primary means of reclaiming space, there is a rough bound on - // the statistic's staleness when available bytes is growing. - // Compactions and flushes are longer, slower operations and provide - // a much looser bound when available bytes is decreasing. - diskAvailBytes func() uint64 } var _ compactionPicker = &compactionPickerByScore{} func (p *compactionPickerByScore) getScores(inProgress []compactionInfo) [numLevels]float64 { var scores [numLevels]float64 - for _, info := range p.calculateScores(inProgress) { - scores[info.level] = info.score + for _, info := range p.calculateLevelScores(inProgress) { + scores[info.level] = info.compensatedScoreRatio } return scores } @@ -787,10 +806,6 @@ func (p *compactionPickerByScore) getBaseLevel() int { return p.baseLevel } -func (p *compactionPickerByScore) getEstimatedMaxWAmp() float64 { - return p.estimatedMaxWAmp -} - // estimatedCompactionDebt estimates the number of bytes which need to be // compacted before the LSM tree becomes stable. func (p *compactionPickerByScore) estimatedCompactionDebt(l0ExtraSize uint64) uint64 { @@ -800,20 +815,22 @@ func (p *compactionPickerByScore) estimatedCompactionDebt(l0ExtraSize uint64) ui // We assume that all the bytes in L0 need to be compacted to Lbase. This is // unlike the RocksDB logic that figures out whether L0 needs compaction. - bytesAddedToNextLevel := l0ExtraSize + uint64(p.levelSizes[0]) - nextLevelSize := uint64(p.levelSizes[p.baseLevel]) + bytesAddedToNextLevel := l0ExtraSize + p.vers.Levels[0].Size() + lbaseSize := p.vers.Levels[p.baseLevel].Size() var compactionDebt uint64 - if bytesAddedToNextLevel > 0 && nextLevelSize > 0 { + if bytesAddedToNextLevel > 0 && lbaseSize > 0 { // We only incur compaction debt if both L0 and Lbase contain data. If L0 // is empty, no compaction is necessary. If Lbase is empty, a move-based // compaction from L0 would occur. - compactionDebt += bytesAddedToNextLevel + nextLevelSize + compactionDebt += bytesAddedToNextLevel + lbaseSize } + // loop invariant: At the beginning of the loop, bytesAddedToNextLevel is the + // bytes added to `level` in the loop. for level := p.baseLevel; level < numLevels-1; level++ { - levelSize := nextLevelSize + bytesAddedToNextLevel - nextLevelSize = uint64(p.levelSizes[level+1]) + levelSize := p.vers.Levels[level].Size() + bytesAddedToNextLevel + nextLevelSize := p.vers.Levels[level+1].Size() if levelSize > uint64(p.levelMaxBytes[level]) { bytesAddedToNextLevel = levelSize - uint64(p.levelMaxBytes[level]) if nextLevelSize > 0 { @@ -824,9 +841,11 @@ func (p *compactionPickerByScore) estimatedCompactionDebt(l0ExtraSize uint64) ui // The next level contributes levelRatio * bytesAddedToNextLevel. compactionDebt += uint64(float64(bytesAddedToNextLevel) * (levelRatio + 1)) } + } else { + // We're not moving any bytes to the next level. + bytesAddedToNextLevel = 0 } } - return compactionDebt } @@ -851,13 +870,13 @@ func (p *compactionPickerByScore) initLevelMaxBytes(inProgressCompactions []comp // Determine the first non-empty level and the total DB size. firstNonEmptyLevel := -1 - var dbSize int64 + var dbSize uint64 for level := 1; level < numLevels; level++ { - if p.levelSizes[level] > 0 { + if p.vers.Levels[level].Size() > 0 { if firstNonEmptyLevel == -1 { firstNonEmptyLevel = level } - dbSize += p.levelSizes[level] + dbSize += p.vers.Levels[level].Size() } } for _, c := range inProgressCompactions { @@ -886,20 +905,20 @@ func (p *compactionPickerByScore) initLevelMaxBytes(inProgressCompactions []comp return } - dbSize += p.levelSizes[0] - bottomLevelSize := dbSize - dbSize/int64(p.opts.Experimental.LevelMultiplier) + dbSize += p.vers.Levels[0].Size() + bottomLevelSize := dbSize - dbSize/uint64(p.opts.Experimental.LevelMultiplier) curLevelSize := bottomLevelSize for level := numLevels - 2; level >= firstNonEmptyLevel; level-- { - curLevelSize = int64(float64(curLevelSize) / float64(p.opts.Experimental.LevelMultiplier)) + curLevelSize = uint64(float64(curLevelSize) / float64(p.opts.Experimental.LevelMultiplier)) } // Compute base level (where L0 data is compacted to). - baseBytesMax := p.opts.LBaseMaxBytes + baseBytesMax := uint64(p.opts.LBaseMaxBytes) p.baseLevel = firstNonEmptyLevel for p.baseLevel > 1 && curLevelSize > baseBytesMax { p.baseLevel-- - curLevelSize = int64(float64(curLevelSize) / float64(p.opts.Experimental.LevelMultiplier)) + curLevelSize = uint64(float64(curLevelSize) / float64(p.opts.Experimental.LevelMultiplier)) } smoothedLevelMultiplier := 1.0 @@ -909,8 +928,6 @@ func (p *compactionPickerByScore) initLevelMaxBytes(inProgressCompactions []comp 1.0/float64(numLevels-p.baseLevel-1)) } - p.estimatedMaxWAmp = float64(numLevels-p.baseLevel) * (smoothedLevelMultiplier + 1) - levelSize := float64(baseBytesMax) for level := p.baseLevel; level < numLevels; level++ { if level > p.baseLevel && levelSize > 0 { @@ -928,16 +945,16 @@ func (p *compactionPickerByScore) initLevelMaxBytes(inProgressCompactions []comp } type levelSizeAdjust struct { - incomingActualBytes int64 - outgoingActualBytes int64 - outgoingCompensatedBytes int64 + incomingActualBytes uint64 + outgoingActualBytes uint64 + outgoingCompensatedBytes uint64 } -func (a levelSizeAdjust) compensated() int64 { +func (a levelSizeAdjust) compensated() uint64 { return a.incomingActualBytes - a.outgoingCompensatedBytes } -func (a levelSizeAdjust) actual() int64 { +func (a levelSizeAdjust) actual() uint64 { return a.incomingActualBytes - a.outgoingActualBytes } @@ -961,10 +978,16 @@ func calculateSizeAdjust(inProgressCompactions []compactionInfo) [numLevels]leve var sizeAdjust [numLevels]levelSizeAdjust for i := range inProgressCompactions { c := &inProgressCompactions[i] + // If this compaction's version edit has already been applied, there's + // no need to adjust: The LSM we'll examine will already reflect the + // new LSM state. + if c.versionEditApplied { + continue + } for _, input := range c.inputs { - actualSize := int64(input.files.SizeSum()) - compensatedSize := int64(totalCompensatedSize(input.files.Iter())) + actualSize := input.files.SizeSum() + compensatedSize := totalCompensatedSize(input.files.Iter()) if input.level != c.outputLevel { sizeAdjust[input.level].outgoingCompensatedBytes += compensatedSize @@ -982,7 +1005,7 @@ func levelCompensatedSize(lm manifest.LevelMetadata) uint64 { return *lm.Annotation(compensatedSizeAnnotator{}).(*uint64) } -func (p *compactionPickerByScore) calculateScores( +func (p *compactionPickerByScore) calculateLevelScores( inProgressCompactions []compactionInfo, ) [numLevels]candidateLevelInfo { var scores [numLevels]candidateLevelInfo @@ -990,72 +1013,89 @@ func (p *compactionPickerByScore) calculateScores( scores[i].level = i scores[i].outputLevel = i + 1 } - scores[0] = p.calculateL0Score(inProgressCompactions) - + l0UncompensatedScore := calculateL0UncompensatedScore(p.vers, p.opts, inProgressCompactions) + scores[0] = candidateLevelInfo{ + outputLevel: p.baseLevel, + uncompensatedScore: l0UncompensatedScore, + compensatedScore: l0UncompensatedScore, /* No level size compensation for L0 */ + } sizeAdjust := calculateSizeAdjust(inProgressCompactions) for level := 1; level < numLevels; level++ { - compensatedLevelSize := int64(levelCompensatedSize(p.vers.Levels[level])) + sizeAdjust[level].compensated() - scores[level].score = float64(compensatedLevelSize) / float64(p.levelMaxBytes[level]) - scores[level].origScore = scores[level].score - - // In addition to the compensated score, we calculate a separate score - // that uses actual file sizes, not compensated sizes. This is used - // during score smoothing down below to prevent excessive - // prioritization of reclaiming disk space. - scores[level].rawScore = float64(p.levelSizes[level]+sizeAdjust[level].actual()) / float64(p.levelMaxBytes[level]) + compensatedLevelSize := levelCompensatedSize(p.vers.Levels[level]) + sizeAdjust[level].compensated() + scores[level].compensatedScore = float64(compensatedLevelSize) / float64(p.levelMaxBytes[level]) + scores[level].uncompensatedScore = float64(p.vers.Levels[level].Size()+sizeAdjust[level].actual()) / float64(p.levelMaxBytes[level]) } - // Adjust each level's score by the score of the next level. If the next - // level has a high score, and is thus a priority for compaction, this - // reduces the priority for compacting the current level. If the next level - // has a low score (i.e. it is below its target size), this increases the - // priority for compacting the current level. + // Adjust each level's {compensated, uncompensated}Score by the uncompensatedScore + // of the next level to get a {compensated, uncompensated}ScoreRatio. If the + // next level has a high uncompensatedScore, and is thus a priority for compaction, + // this reduces the priority for compacting the current level. If the next level + // has a low uncompensatedScore (i.e. it is below its target size), this increases + // the priority for compacting the current level. // // The effect of this adjustment is to help prioritize compactions in lower - // levels. The following shows the new score and original score. In this - // scenario, L0 has 68 sublevels. L3 (a.k.a. Lbase) is significantly above - // its target size. The original score prioritizes compactions from those two - // levels, but doing so ends up causing a future problem: data piles up in - // the higher levels, starving L5->L6 compactions, and to a lesser degree - // starving L4->L5 compactions. + // levels. The following example shows the compensatedScoreRatio and the + // compensatedScore. In this scenario, L0 has 68 sublevels. L3 (a.k.a. Lbase) + // is significantly above its target size. The original score prioritizes + // compactions from those two levels, but doing so ends up causing a future + // problem: data piles up in the higher levels, starving L5->L6 compactions, + // and to a lesser degree starving L4->L5 compactions. // - // adjusted original - // score score size max-size - // L0 3.2 68.0 2.2 G - - // L3 3.2 21.1 1.3 G 64 M - // L4 3.4 6.7 3.1 G 467 M - // L5 3.4 2.0 6.6 G 3.3 G - // L6 0.6 0.6 14 G 24 G + // Note that in the example shown there is no level size compensation so the + // compensatedScore and the uncompensatedScore is the same for each level. + // + // compensatedScoreRatio compensatedScore uncompensatedScore size max-size + // L0 3.2 68.0 68.0 2.2 G - + // L3 3.2 21.1 21.1 1.3 G 64 M + // L4 3.4 6.7 6.7 3.1 G 467 M + // L5 3.4 2.0 2.0 6.6 G 3.3 G + // L6 0.6 0.6 0.6 14 G 24 G var prevLevel int for level := p.baseLevel; level < numLevels; level++ { - if scores[prevLevel].score >= 1 { - // Avoid absurdly large scores by placing a floor on the score that we'll - // adjust a level by. The value of 0.01 was chosen somewhat arbitrarily - const minScore = 0.01 - if scores[level].rawScore >= minScore { - scores[prevLevel].score /= scores[level].rawScore + // The compensated scores, and uncompensated scores will be turned into + // ratios as they're adjusted according to other levels' sizes. + scores[prevLevel].compensatedScoreRatio = scores[prevLevel].compensatedScore + scores[prevLevel].uncompensatedScoreRatio = scores[prevLevel].uncompensatedScore + + // Avoid absurdly large scores by placing a floor on the score that we'll + // adjust a level by. The value of 0.01 was chosen somewhat arbitrarily. + const minScore = 0.01 + if scores[prevLevel].compensatedScoreRatio >= compactionScoreThreshold { + if scores[level].uncompensatedScore >= minScore { + scores[prevLevel].compensatedScoreRatio /= scores[level].uncompensatedScore } else { - scores[prevLevel].score /= minScore + scores[prevLevel].compensatedScoreRatio /= minScore + } + } + if scores[prevLevel].uncompensatedScoreRatio >= compactionScoreThreshold { + if scores[level].uncompensatedScore >= minScore { + scores[prevLevel].uncompensatedScoreRatio /= scores[level].uncompensatedScore + } else { + scores[prevLevel].uncompensatedScoreRatio /= minScore } } prevLevel = level } + // Set the score ratios for the lowest level. + // INVARIANT: prevLevel == numLevels-1 + scores[prevLevel].compensatedScoreRatio = scores[prevLevel].compensatedScore + scores[prevLevel].uncompensatedScoreRatio = scores[prevLevel].uncompensatedScore - sort.Sort(sortCompactionLevelsDecreasingScore(scores[:])) + sort.Sort(sortCompactionLevelsByPriority(scores[:])) return scores } -func (p *compactionPickerByScore) calculateL0Score( - inProgressCompactions []compactionInfo, -) candidateLevelInfo { - var info candidateLevelInfo - info.outputLevel = p.baseLevel - - // If L0Sublevels are present, use the sublevel count to calculate the - // score. The base vs intra-L0 compaction determination happens in pickAuto, - // not here. - info.score = float64(2*p.vers.L0Sublevels.MaxDepthAfterOngoingCompactions()) / - float64(p.opts.L0CompactionThreshold) +// calculateL0UncompensatedScore calculates a float score representing the +// relative priority of compacting L0. Level L0 is special in that files within +// L0 may overlap one another, so a different set of heuristics that take into +// account read amplification apply. +func calculateL0UncompensatedScore( + vers *version, opts *Options, inProgressCompactions []compactionInfo, +) float64 { + // Use the sublevel count to calculate the score. The base vs intra-L0 + // compaction determination happens in pickAuto, not here. + score := float64(2*vers.L0Sublevels.MaxDepthAfterOngoingCompactions()) / + float64(opts.L0CompactionThreshold) // Also calculate a score based on the file count but use it only if it // produces a higher score than the sublevel-based one. This heuristic is @@ -1065,7 +1105,7 @@ func (p *compactionPickerByScore) calculateL0Score( // 1) we can produce a massive backlog to compact once files do overlap. // 2) constructing L0 sublevels has a runtime that grows superlinearly with // the number of files in L0 and must be done while holding D.mu. - noncompactingFiles := p.vers.Levels[0].Len() + noncompactingFiles := vers.Levels[0].Len() for _, c := range inProgressCompactions { for _, cl := range c.inputs { if cl.level == 0 { @@ -1073,15 +1113,20 @@ func (p *compactionPickerByScore) calculateL0Score( } } } - fileScore := float64(noncompactingFiles) / float64(p.opts.L0CompactionFileThreshold) - if info.score < fileScore { - info.score = fileScore + fileScore := float64(noncompactingFiles) / float64(opts.L0CompactionFileThreshold) + if score < fileScore { + score = fileScore } - return info + return score } -func (p *compactionPickerByScore) pickFile( - level, outputLevel int, earliestSnapshotSeqNum uint64, +// pickCompactionSeedFile picks a file from `level` in the `vers` to build a +// compaction around. Currently, this function implements a heuristic similar to +// RocksDB's kMinOverlappingRatio, seeking to minimize write amplification. This +// function is linear with respect to the number of files in `level` and +// `outputLevel`. +func pickCompactionSeedFile( + vers *version, opts *Options, level, outputLevel int, earliestSnapshotSeqNum uint64, ) (manifest.LevelFile, bool) { // Select the file within the level to compact. We want to minimize write // amplification, but also ensure that deletes are propagated to the @@ -1105,9 +1150,9 @@ func (p *compactionPickerByScore) pickFile( // pick a seed file whose resulting compaction bounds do not overlap with // an in-progress compaction. - cmp := p.opts.Comparer.Compare - startIter := p.vers.Levels[level].Iter() - outputIter := p.vers.Levels[outputLevel].Iter() + cmp := opts.Comparer.Compare + startIter := vers.Levels[level].Iter() + outputIter := vers.Levels[outputLevel].Iter() var file manifest.LevelFile smallestRatio := uint64(math.MaxUint64) @@ -1116,14 +1161,20 @@ func (p *compactionPickerByScore) pickFile( for f := startIter.First(); f != nil; f = startIter.Next() { var overlappingBytes uint64 + compacting := f.IsCompacting() + if compacting { + // Move on if this file is already being compacted. We'll likely + // still need to move past the overlapping output files regardless, + // but in cases where all start-level files are compacting we won't. + continue + } // Trim any output-level files smaller than f. - for outputFile != nil && base.InternalCompare(cmp, outputFile.Largest, f.Smallest) < 0 { + for outputFile != nil && sstableKeyCompare(cmp, outputFile.Largest, f.Smallest) < 0 { outputFile = outputIter.Next() } - compacting := f.IsCompacting() - for outputFile != nil && base.InternalCompare(cmp, outputFile.Smallest, f.Largest) < 0 { + for outputFile != nil && sstableKeyCompare(cmp, outputFile.Smallest, f.Largest) <= 0 && !compacting { overlappingBytes += outputFile.Size compacting = compacting || outputFile.IsCompacting() @@ -1142,7 +1193,32 @@ func (p *compactionPickerByScore) pickFile( // If the file in the next level extends beyond f's largest key, // break out and don't advance outputIter because f's successor // might also overlap. - if base.InternalCompare(cmp, outputFile.Largest, f.Largest) > 0 { + // + // Note, we stop as soon as we encounter an output-level file with a + // largest key beyond the input-level file's largest bound. We + // perform a simple user key comparison here using sstableKeyCompare + // which handles the potential for exclusive largest key bounds. + // There's some subtlety when the bounds are equal (eg, equal and + // inclusive, or equal and exclusive). Current Pebble doesn't split + // user keys across sstables within a level (and in format versions + // FormatSplitUserKeysMarkedCompacted and later we guarantee no + // split user keys exist within the entire LSM). In that case, we're + // assured that neither the input level nor the output level's next + // file shares the same user key, so compaction expansion will not + // include them in any compaction compacting `f`. + // + // NB: If we /did/ allow split user keys, or we're running on an + // old database with an earlier format major version where there are + // existing split user keys, this logic would be incorrect. Consider + // L1: [a#120,a#100] [a#80,a#60] + // L2: [a#55,a#45] [a#35,a#25] [a#15,a#5] + // While considering the first file in L1, [a#120,a#100], we'd skip + // past all of the files in L2. When considering the second file in + // L1, we'd improperly conclude that the second file overlaps + // nothing in the second level and is cheap to compact, when in + // reality we'd need to expand the compaction to include all 5 + // files. + if sstableKeyCompare(cmp, outputFile.Largest, f.Largest) > 0 { break } outputFile = outputIter.Next() @@ -1157,7 +1233,7 @@ func (p *compactionPickerByScore) pickFile( compSz := compensatedSize(f) scaledRatio := overlappingBytes * 1024 / compSz - if scaledRatio < smallestRatio && !f.IsCompacting() { + if scaledRatio < smallestRatio { smallestRatio = scaledRatio file = startIter.Take() } @@ -1185,15 +1261,15 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact // bytes have been compacted further down the LSM. if n := len(env.inProgressCompactions); n > 0 { l0ReadAmp := p.vers.L0Sublevels.MaxDepthAfterOngoingCompactions() - compactionDebt := int(p.estimatedCompactionDebt(0)) + compactionDebt := p.estimatedCompactionDebt(0) ccSignal1 := n * p.opts.Experimental.L0CompactionConcurrency - ccSignal2 := n * p.opts.Experimental.CompactionDebtConcurrency + ccSignal2 := uint64(n) * p.opts.Experimental.CompactionDebtConcurrency if l0ReadAmp < ccSignal1 && compactionDebt < ccSignal2 { return nil } } - scores := p.calculateScores(env.inProgressCompactions) + scores := p.calculateLevelScores(env.inProgressCompactions) // TODO(bananabrick): Either remove, or change this into an event sent to the // EventListener. @@ -1216,12 +1292,13 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact if pc.startLevel.level == info.level { marker = "*" } - fmt.Fprintf(&buf, " %sL%d: %5.1f %5.1f %5.1f %8s %8s", - marker, info.level, info.score, info.origScore, info.rawScore, - humanize.Int64(int64(totalCompensatedSize( + fmt.Fprintf(&buf, " %sL%d: %5.1f %5.1f %5.1f %5.1f %8s %8s", + marker, info.level, info.compensatedScoreRatio, info.compensatedScore, + info.uncompensatedScoreRatio, info.uncompensatedScore, + humanize.Bytes.Int64(int64(totalCompensatedSize( p.vers.Levels[info.level].Iter(), ))), - humanize.Int64(p.levelMaxBytes[info.level]), + humanize.Bytes.Int64(p.levelMaxBytes[info.level]), ) count := 0 @@ -1247,12 +1324,12 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact pc.startLevel.level, pc.outputLevel.level, buf.String()) } - // Check for a score-based compaction. "scores" has been sorted in order of - // decreasing score. For each level with a score >= 1, we attempt to find a - // compaction anchored at at that level. + // Check for a score-based compaction. candidateLevelInfos are first sorted + // by whether they should be compacted, so if we find a level which shouldn't + // be compacted, we can break early. for i := range scores { info := &scores[i] - if info.score < 1 { + if !info.shouldCompact() { break } if info.level == numLevels-1 { @@ -1260,11 +1337,12 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact } if info.level == 0 { - pc = pickL0(env, p.opts, p.vers, p.baseLevel, p.diskAvailBytes) + pc = pickL0(env, p.opts, p.vers, p.baseLevel) // Fail-safe to protect against compacting the same sstable // concurrently. if pc != nil && !inputRangeAlreadyCompacting(env, pc) { - pc.score = info.score + p.addScoresToPickedCompactionMetrics(pc, scores) + pc.score = info.compensatedScoreRatio // TODO(bananabrick): Create an EventListener for logCompaction. if false { logCompaction(pc) @@ -1276,15 +1354,16 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact // info.level > 0 var ok bool - info.file, ok = p.pickFile(info.level, info.outputLevel, env.earliestSnapshotSeqNum) + info.file, ok = pickCompactionSeedFile(p.vers, p.opts, info.level, info.outputLevel, env.earliestSnapshotSeqNum) if !ok { continue } - pc := pickAutoLPositive(env, p.opts, p.vers, *info, p.baseLevel, p.diskAvailBytes, p.levelMaxBytes) + pc := pickAutoLPositive(env, p.opts, p.vers, *info, p.baseLevel, p.levelMaxBytes) // Fail-safe to protect against compacting the same sstable concurrently. if pc != nil && !inputRangeAlreadyCompacting(env, pc) { - pc.score = info.score + p.addScoresToPickedCompactionMetrics(pc, scores) + pc.score = info.compensatedScoreRatio // TODO(bananabrick): Create an EventListener for logCompaction. if false { logCompaction(pc) @@ -1345,6 +1424,30 @@ func (p *compactionPickerByScore) pickAuto(env compactionEnv) (pc *pickedCompact return nil } +func (p *compactionPickerByScore) addScoresToPickedCompactionMetrics( + pc *pickedCompaction, candInfo [numLevels]candidateLevelInfo, +) { + + // candInfo is sorted by score, not by compaction level. + infoByLevel := [numLevels]candidateLevelInfo{} + for i := range candInfo { + level := candInfo[i].level + infoByLevel[level] = candInfo[i] + } + // Gather the compaction scores for the levels participating in the compaction. + pc.pickerMetrics.scores = make([]float64, len(pc.inputs)) + inputIdx := 0 + for i := range infoByLevel { + if pc.inputs[inputIdx].level == infoByLevel[i].level { + pc.pickerMetrics.scores[inputIdx] = infoByLevel[i].compensatedScoreRatio + inputIdx++ + } + if inputIdx == len(pc.inputs) { + break + } + } +} + // elisionOnlyAnnotator implements the manifest.Annotator interface, // annotating B-Tree nodes with the *fileMetadata of a file meeting the // obsolete keys criteria for an elision-only compaction within the subtree. @@ -1364,7 +1467,7 @@ func (a elisionOnlyAnnotator) Accumulate(f *fileMetadata, dst interface{}) (inte if f.IsCompacting() { return dst, true } - if !f.StatsValidLocked() { + if !f.StatsValid() { return dst, false } // Bottommost files are large and not worthwhile to compact just @@ -1543,7 +1646,7 @@ func (p *compactionPickerByScore) pickRewriteCompaction(env compactionEnv) (pc * // Fail-safe to protect against compacting the same sstable concurrently. if !inputRangeAlreadyCompacting(env, pc) { if pc.startLevel.level == 0 { - pc.l0SublevelInfo = generateSublevelInfo(pc.cmp, pc.startLevel.files) + pc.startLevel.l0SublevelInfo = generateSublevelInfo(pc.cmp, pc.startLevel.files) } return pc } @@ -1560,7 +1663,6 @@ func pickAutoLPositive( vers *version, cInfo candidateLevelInfo, baseLevel int, - diskAvailBytes func() uint64, levelMaxBytes [7]int64, ) (pc *pickedCompaction) { if cInfo.level == 0 { @@ -1583,46 +1685,124 @@ func pickAutoLPositive( } } - if !pc.setupInputs(opts, diskAvailBytes(), pc.startLevel) { + if !pc.setupInputs(opts, env.diskAvailBytes, pc.startLevel) { return nil } - return pc.maybeAddLevel(opts, diskAvailBytes()) + return pc.maybeAddLevel(opts, env.diskAvailBytes) } // maybeAddLevel maybe adds a level to the picked compaction. func (pc *pickedCompaction) maybeAddLevel(opts *Options, diskAvailBytes uint64) *pickedCompaction { + pc.pickerMetrics.singleLevelOverlappingRatio = pc.overlappingRatio() if pc.outputLevel.level == numLevels-1 { // Don't add a level if the current output level is in L6 return pc } + if !opts.Experimental.MultiLevelCompactionHeuristic.allowL0() && pc.startLevel.level == 0 { + return pc + } if pc.compactionSize() > expandedCompactionByteSizeLimit( - opts, pc.adjustedOutputLevel, diskAvailBytes) { + opts, adjustedOutputLevel(pc.outputLevel.level, pc.baseLevel), diskAvailBytes) { // Don't add a level if the current compaction exceeds the compaction size limit return pc } - return opts.Experimental.MultiLevelCompactionHueristic.pick(pc, opts, diskAvailBytes) + return opts.Experimental.MultiLevelCompactionHeuristic.pick(pc, opts, diskAvailBytes) } // MultiLevelHeuristic evaluates whether to add files from the next level into the compaction. type MultiLevelHeuristic interface { - // evaluate returns the preferred compaction. + // Evaluate returns the preferred compaction. pick(pc *pickedCompaction, opts *Options, diskAvailBytes uint64) *pickedCompaction + + // Returns if the heuristic allows L0 to be involved in ML compaction + allowL0() bool } // NoMultiLevel will never add an additional level to the compaction. type NoMultiLevel struct{} +var _ MultiLevelHeuristic = (*NoMultiLevel)(nil) + func (nml NoMultiLevel) pick( pc *pickedCompaction, opts *Options, diskAvailBytes uint64, ) *pickedCompaction { return pc } +func (nml NoMultiLevel) allowL0() bool { + return false +} + +func (pc *pickedCompaction) predictedWriteAmp() float64 { + var bytesToCompact uint64 + var higherLevelBytes uint64 + for i := range pc.inputs { + levelSize := pc.inputs[i].files.SizeSum() + bytesToCompact += levelSize + if i != len(pc.inputs)-1 { + higherLevelBytes += levelSize + } + } + return float64(bytesToCompact) / float64(higherLevelBytes) +} + +func (pc *pickedCompaction) overlappingRatio() float64 { + var higherLevelBytes uint64 + var lowestLevelBytes uint64 + for i := range pc.inputs { + levelSize := pc.inputs[i].files.SizeSum() + if i == len(pc.inputs)-1 { + lowestLevelBytes += levelSize + continue + } + higherLevelBytes += levelSize + } + return float64(lowestLevelBytes) / float64(higherLevelBytes) +} + +// WriteAmpHeuristic defines a multi level compaction heuristic which will add +// an additional level to the picked compaction if it reduces predicted write +// amp of the compaction + the addPropensity constant. +type WriteAmpHeuristic struct { + // addPropensity is a constant that affects the propensity to conduct multilevel + // compactions. If positive, a multilevel compaction may get picked even if + // the single level compaction has lower write amp, and vice versa. + AddPropensity float64 + + // AllowL0 if true, allow l0 to be involved in a ML compaction. + AllowL0 bool +} + +var _ MultiLevelHeuristic = (*WriteAmpHeuristic)(nil) + +// TODO(msbutler): microbenchmark the extent to which multilevel compaction +// picking slows down the compaction picking process. This should be as fast as +// possible since Compaction-picking holds d.mu, which prevents WAL rotations, +// in-progress flushes and compactions from completing, etc. Consider ways to +// deduplicate work, given that setupInputs has already been called. +func (wa WriteAmpHeuristic) pick( + pcOrig *pickedCompaction, opts *Options, diskAvailBytes uint64, +) *pickedCompaction { + pcMulti := pcOrig.clone() + if !pcMulti.setupMultiLevelCandidate(opts, diskAvailBytes) { + return pcOrig + } + picked := pcOrig + if pcMulti.predictedWriteAmp() <= pcOrig.predictedWriteAmp()+wa.AddPropensity { + picked = pcMulti + } + // Regardless of what compaction was picked, log the multilevelOverlapping ratio. + picked.pickerMetrics.multiLevelOverlappingRatio = pcMulti.overlappingRatio() + return picked +} + +func (wa WriteAmpHeuristic) allowL0() bool { + return wa.AllowL0 +} + // Helper method to pick compactions originating from L0. Uses information about // sublevels to generate a compaction. -func pickL0( - env compactionEnv, opts *Options, vers *version, baseLevel int, diskAvailBytes func() uint64, -) (pc *pickedCompaction) { +func pickL0(env compactionEnv, opts *Options, vers *version, baseLevel int) (pc *pickedCompaction) { // It is important to pass information about Lbase files to L0Sublevels // so it can pick a compaction that does not conflict with an Lbase => Lbase+1 // compaction. Without this, we observed reduced concurrency of L0=>Lbase @@ -1637,11 +1817,11 @@ func pickL0( } if lcf != nil { pc = newPickedCompactionFromL0(lcf, opts, vers, baseLevel, true) - pc.setupInputs(opts, diskAvailBytes(), pc.startLevel) + pc.setupInputs(opts, env.diskAvailBytes, pc.startLevel) if pc.startLevel.files.Empty() { opts.Logger.Fatalf("empty compaction chosen") } - return pc + return pc.maybeAddLevel(opts, env.diskAvailBytes) } // Couldn't choose a base compaction. Try choosing an intra-L0 @@ -1655,7 +1835,7 @@ func pickL0( } if lcf != nil { pc = newPickedCompactionFromL0(lcf, opts, vers, 0, false) - if !pc.setupInputs(opts, diskAvailBytes(), pc.startLevel) { + if !pc.setupInputs(opts, env.diskAvailBytes, pc.startLevel) { return nil } if pc.startLevel.files.Empty() { @@ -1674,17 +1854,13 @@ func pickL0( return pc } -func (p *compactionPickerByScore) pickManual( - env compactionEnv, manual *manualCompaction, +func pickManualCompaction( + vers *version, opts *Options, env compactionEnv, baseLevel int, manual *manualCompaction, ) (pc *pickedCompaction, retryLater bool) { - if p == nil { - return nil, false - } - outputLevel := manual.level + 1 if manual.level == 0 { - outputLevel = p.baseLevel - } else if manual.level < p.baseLevel { + outputLevel = baseLevel + } else if manual.level < baseLevel { // The start level for a compaction must be >= Lbase. A manual // compaction could have been created adhering to that condition, and // then an automatic compaction came in and compacted all of the @@ -1698,11 +1874,22 @@ func (p *compactionPickerByScore) pickManual( // it, the compaction is dropped due to pc.setupInputs returning false since // the input/output range is already being compacted, and the manual // compaction ends with a non-compacted LSM. - if conflictsWithInProgress(manual, outputLevel, env.inProgressCompactions, p.opts.Comparer.Compare) { + if conflictsWithInProgress(manual, outputLevel, env.inProgressCompactions, opts.Comparer.Compare) { return nil, true } - pc = pickManualHelper(p.opts, manual, p.vers, p.baseLevel, p.diskAvailBytes, p.levelMaxBytes) - if pc == nil { + pc = newPickedCompaction(opts, vers, manual.level, defaultOutputLevel(manual.level, baseLevel), baseLevel) + manual.outputLevel = pc.outputLevel.level + pc.startLevel.files = vers.Overlaps(manual.level, opts.Comparer.Compare, manual.start, manual.end, false) + if pc.startLevel.files.Empty() { + // Nothing to do + return nil, false + } + if !pc.setupInputs(opts, env.diskAvailBytes, pc.startLevel) { + // setupInputs returned false indicating there's a conflicting + // concurrent compaction. + return nil, true + } + if pc = pc.maybeAddLevel(opts, env.diskAvailBytes); pc == nil { return nil, false } if pc.outputLevel.level != outputLevel { @@ -1719,28 +1906,6 @@ func (p *compactionPickerByScore) pickManual( return pc, false } -func pickManualHelper( - opts *Options, - manual *manualCompaction, - vers *version, - baseLevel int, - diskAvailBytes func() uint64, - levelMaxBytes [7]int64, -) (pc *pickedCompaction) { - pc = newPickedCompaction(opts, vers, manual.level, defaultOutputLevel(manual.level, baseLevel), baseLevel) - manual.outputLevel = pc.outputLevel.level - cmp := opts.Comparer.Compare - pc.startLevel.files = vers.Overlaps(manual.level, cmp, manual.start, manual.end, false) - if pc.startLevel.files.Empty() { - // Nothing to do - return nil - } - if !pc.setupInputs(opts, diskAvailBytes(), pc.startLevel) { - return nil - } - return pc.maybeAddLevel(opts, diskAvailBytes()) -} - func (p *compactionPickerByScore) pickReadTriggeredCompaction( env compactionEnv, ) (pc *pickedCompaction) { @@ -1786,7 +1951,7 @@ func pickReadTriggeredCompactionHelper( pc = newPickedCompaction(p.opts, p.vers, rc.level, defaultOutputLevel(rc.level, p.baseLevel), p.baseLevel) pc.startLevel.files = overlapSlice - if !pc.setupInputs(p.opts, p.diskAvailBytes(), pc.startLevel) { + if !pc.setupInputs(p.opts, env.diskAvailBytes, pc.startLevel) { return nil } if inputRangeAlreadyCompacting(env, pc) { diff --git a/vendor/github.com/cockroachdb/pebble/db.go b/vendor/github.com/cockroachdb/pebble/db.go index 9ec2d8b..f0d0591 100644 --- a/vendor/github.com/cockroachdb/pebble/db.go +++ b/vendor/github.com/cockroachdb/pebble/db.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "os" + "strconv" "sync" "sync/atomic" "time" @@ -17,15 +18,19 @@ import ( "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/arenaskl" "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invalidating" "github.com/cockroachdb/pebble/internal/invariants" "github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/manual" "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/rangekey" "github.com/cockroachdb/pebble/record" "github.com/cockroachdb/pebble/sstable" "github.com/cockroachdb/pebble/vfs" "github.com/cockroachdb/pebble/vfs/atomicfs" + "github.com/cockroachdb/tokenbucket" "github.com/prometheus/client_golang/prometheus" ) @@ -70,7 +75,7 @@ type Reader interface { // NewIter returns an iterator that is unpositioned (Iterator.Valid() will // return false). The iterator can be positioned via a call to SeekGE, // SeekLT, First or Last. - NewIter(o *IterOptions) *Iterator + NewIter(o *IterOptions) (*Iterator, error) // Close closes the Reader. It may or may not close any underlying io.Reader // or io.Writer, depending on how the DB was created. @@ -96,6 +101,22 @@ type Writer interface { // It is safe to modify the contents of the arguments after Delete returns. Delete(key []byte, o *WriteOptions) error + // DeleteSized behaves identically to Delete, but takes an additional + // argument indicating the size of the value being deleted. DeleteSized + // should be preferred when the caller has the expectation that there exists + // a single internal KV pair for the key (eg, the key has not been + // overwritten recently), and the caller knows the size of its value. + // + // DeleteSized will record the value size within the tombstone and use it to + // inform compaction-picking heuristics which strive to reduce space + // amplification in the LSM. This "calling your shot" mechanic allows the + // storage engine to more accurately estimate and reduce space + // amplification. + // + // It is safe to modify the contents of the arguments after DeleteSized + // returns. + DeleteSized(key []byte, valueSize uint32, _ *WriteOptions) error + // SingleDelete is similar to Delete in that it deletes the value for the given key. Like Delete, // it is a blind operation that will succeed even if the given key does not exist. // @@ -232,25 +253,22 @@ func (d defaultCPUWorkGranter) CPUWorkDone(_ CPUWorkHandle) {} // Comparer: myComparer, // }) type DB struct { - // WARNING: The following struct `atomic` contains fields which are accessed - // atomically. - // - // Go allocations are guaranteed to be 64-bit aligned which we take advantage - // of by placing the 64-bit fields which we access atomically at the beginning - // of the DB struct. For more information, see https://golang.org/pkg/sync/atomic/#pkg-note-BUG. - atomic struct { - // The count and size of referenced memtables. This includes memtables - // present in DB.mu.mem.queue, as well as memtables that have been flushed - // but are still referenced by an inuse readState. - memTableCount int64 - memTableReserved int64 // number of bytes reserved in the cache for memtables + // The count and size of referenced memtables. This includes memtables + // present in DB.mu.mem.queue, as well as memtables that have been flushed + // but are still referenced by an inuse readState, as well as up to one + // memTable waiting to be reused and stored in d.memTableRecycle. + memTableCount atomic.Int64 + memTableReserved atomic.Int64 // number of bytes reserved in the cache for memtables + // memTableRecycle holds a pointer to an obsolete memtable. The next + // memtable allocation will reuse this memtable if it has not already been + // recycled. + memTableRecycle atomic.Pointer[memTable] - // The size of the current log file (i.e. db.mu.log.queue[len(queue)-1]. - logSize uint64 + // The size of the current log file (i.e. db.mu.log.queue[len(queue)-1]. + logSize atomic.Uint64 - // The number of bytes available on disk. - diskAvailBytes uint64 - } + // The number of bytes available on disk. + diskAvailBytes atomic.Uint64 cacheID uint64 dirname string @@ -263,14 +281,14 @@ type DB struct { abbreviatedKey AbbreviatedKey // The threshold for determining when a batch is "large" and will skip being // inserted into a memtable. - largeBatchThreshold int + largeBatchThreshold uint64 // The current OPTIONS file number. - optionsFileNum FileNum + optionsFileNum base.DiskFileNum // The on-disk size of the current OPTIONS file. optionsFileSize uint64 // objProvider is used to access and manage SSTs. - objProvider *objstorage.Provider + objProvider objstorage.Provider fileLock *Lock dataDir vfs.File @@ -297,14 +315,7 @@ type DB struct { closed *atomic.Value closedCh chan struct{} - deletionPacer pacer - - // Async deletion jobs spawned by cleaners increment this WaitGroup, and - // call Done when completed. Once `d.mu.cleaning` is false, the db.Close() - // goroutine needs to call Wait on this WaitGroup to ensure all cleaning - // and deleting goroutines have finished running. As deletion goroutines - // could grab db.mu, it must *not* be held while deleters.Wait() is called. - deleters sync.WaitGroup + cleanupManager *cleanupManager // During an iterator close, we may asynchronously schedule read compactions. // We want to wait for those goroutines to finish, before closing the DB. @@ -382,9 +393,6 @@ type DB struct { } mem struct { - // Condition variable used to serialize memtable switching. See - // DB.makeRoomForWrite(). - cond sync.Cond // The current mutable memTable. mutable *memTable // Queue of flushables (the mutable memtable is at end). Elements are @@ -397,7 +405,7 @@ type DB struct { // is allocated up to Options.MemTableSize. This reduces the memory // footprint of memtables when lots of DB instances are used concurrently // in test environments. - nextSize int + nextSize uint64 } compact struct { @@ -416,6 +424,10 @@ type DB struct { // is at the start of the list. New entries are added to the end. manual []*manualCompaction // inProgress is the set of in-progress flushes and compactions. + // It's used in the calculation of some metrics and to initialize L0 + // sublevels' state. Some of the compactions contained within this + // map may have already committed an edit to the version but are + // lingering performing cleanup, like deleting obsolete files. inProgress map[*compaction]struct{} // rescheduleReadCompaction indicates to an iterator that a read compaction @@ -426,6 +438,9 @@ type DB struct { // compactions which we might have to perform. readCompactions readCompactionQueue + // The cumulative duration of all completed compactions since Open. + // Does not include flushes. + duration time.Duration // Flush throughput metric. flushWriteThroughput ThroughputMetric // The idle start time for the flush "loop", i.e., when the flushing @@ -433,20 +448,10 @@ type DB struct { noOngoingFlushStartTime time.Time } - cleaner struct { - // Condition variable used to signal the completion of a file cleaning - // operation or an increment to the value of disabled. File cleaning operations are - // serialized, and a caller trying to do a file cleaning operation may wait - // until the ongoing one is complete. - cond sync.Cond - // True when a file cleaning operation is in progress. False does not necessarily - // mean all cleaning jobs have completed; see the comment on d.deleters. - cleaning bool - // Non-zero when file cleaning is disabled. The disabled count acts as a - // reference count to prohibit file cleaning. See - // DB.{disable,Enable}FileDeletions(). - disabled int - } + // Non-zero when file cleaning is disabled. The disabled count acts as a + // reference count to prohibit file cleaning. See + // DB.{disable,Enable}FileDeletions(). + disableFileDeletions int snapshots struct { // The list of active snapshots. @@ -481,7 +486,8 @@ type DB struct { // job to validate one or more sstables. cond sync.Cond // pending is a slice of metadata for sstables waiting to be - // validated. + // validated. Only physical sstables should be added to the pending + // queue. pending []newFileEntry // validating is set to true when validation is running. validating bool @@ -490,6 +496,9 @@ type DB struct { // Normally equal to time.Now() but may be overridden in tests. timeNow func() time.Time + // the time at database Open; may be used to compute metrics like effective + // compaction concurrency + openedAt time.Time } var _ Reader = (*DB)(nil) @@ -497,11 +506,7 @@ var _ Writer = (*DB)(nil) // TestOnlyWaitForCleaning MUST only be used in tests. func (d *DB) TestOnlyWaitForCleaning() { - d.mu.Lock() - defer d.mu.Unlock() - for d.mu.cleaner.cleaning { - d.mu.cleaner.cond.Wait() - } + d.cleanupManager.Wait() } // Get gets the value for the given key. It returns ErrNotFound if the DB does @@ -543,7 +548,7 @@ func (d *DB) getInternal(key []byte, b *Batch, s *Snapshot) ([]byte, io.Closer, if s != nil { seqNum = s.seqNum } else { - seqNum = atomic.LoadUint64(&d.mu.versions.atomic.visibleSeqNum) + seqNum = d.mu.versions.visibleSeqNum.Load() } buf := getIterAllocPool.Get().(*getIterAlloc) @@ -551,8 +556,7 @@ func (d *DB) getInternal(key []byte, b *Batch, s *Snapshot) ([]byte, io.Closer, get := &buf.get *get = getIter{ logger: d.opts.Logger, - cmp: d.cmp, - equal: d.equal, + comparer: d.opts.Comparer, newIters: d.newIters, snapshot: seqNum, key: key, @@ -625,6 +629,30 @@ func (d *DB) Delete(key []byte, opts *WriteOptions) error { return nil } +// DeleteSized behaves identically to Delete, but takes an additional +// argument indicating the size of the value being deleted. DeleteSized +// should be preferred when the caller has the expectation that there exists +// a single internal KV pair for the key (eg, the key has not been +// overwritten recently), and the caller knows the size of its value. +// +// DeleteSized will record the value size within the tombstone and use it to +// inform compaction-picking heuristics which strive to reduce space +// amplification in the LSM. This "calling your shot" mechanic allows the +// storage engine to more accurately estimate and reduce space amplification. +// +// It is safe to modify the contents of the arguments after DeleteSized +// returns. +func (d *DB) DeleteSized(key []byte, valueSize uint32, opts *WriteOptions) error { + b := newBatch(d) + _ = b.DeleteSized(key, valueSize, opts) + if err := d.Apply(b, opts); err != nil { + return err + } + // Only release the batch on success. + b.release() + return nil +} + // SingleDelete adds an action to the batch that single deletes the entry for key. // See Writer.SingleDelete for more details on the semantics of SingleDelete. // @@ -776,7 +804,10 @@ func (d *DB) applyInternal(batch *Batch, opts *WriteOptions, noSyncWait bool) er if err := d.closed.Load(); err != nil { panic(err) } - if atomic.LoadUint32(&batch.applied) != 0 { + if batch.committing { + panic("pebble: batch already committing") + } + if batch.applied.Load() { panic("pebble: batch already applied") } if d.opts.ReadOnly { @@ -791,25 +822,34 @@ func (d *DB) applyInternal(batch *Batch, opts *WriteOptions, noSyncWait bool) er return errors.New("pebble: WAL disabled") } + if batch.minimumFormatMajorVersion != FormatMostCompatible { + if fmv := d.FormatMajorVersion(); fmv < batch.minimumFormatMajorVersion { + panic(fmt.Sprintf( + "pebble: batch requires at least format major version %d (current: %d)", + batch.minimumFormatMajorVersion, fmv, + )) + } + } + if batch.countRangeKeys > 0 { if d.split == nil { return errNoSplit } - if d.FormatMajorVersion() < FormatRangeKeys { - panic(fmt.Sprintf( - "pebble: range keys require at least format major version %d (current: %d)", - FormatRangeKeys, d.FormatMajorVersion(), - )) - } - // TODO(jackson): Assert that all range key operands are suffixless. } + batch.committing = true if batch.db == nil { - batch.refreshMemTableSize() + if err := batch.refreshMemTableSize(); err != nil { + return err + } } - if int(batch.memTableSize) >= d.largeBatchThreshold { - batch.flushable = newFlushableBatch(batch, d.opts.Comparer) + if batch.memTableSize >= d.largeBatchThreshold { + var err error + batch.flushable, err = newFlushableBatch(batch, d.opts.Comparer) + if err != nil { + return err + } } if err := d.commit.Commit(batch, sync, noSyncWait); err != nil { // There isn't much we can do on an error here. The commit pipeline will be @@ -882,7 +922,7 @@ func (d *DB) commitWrite(b *Batch, syncWG *sync.WaitGroup, syncErr *error) (*mem b.flushable.setSeqNum(b.SeqNum()) if !d.opts.DisableWAL { var err error - size, b.commitStats.WALQueueWaitDuration, err = d.mu.log.SyncRecord(repr, syncWG, syncErr) + size, err = d.mu.log.SyncRecord(repr, syncWG, syncErr) if err != nil { panic(err) } @@ -920,13 +960,13 @@ func (d *DB) commitWrite(b *Batch, syncWG *sync.WaitGroup, syncErr *error) (*mem } if b.flushable == nil { - size, b.commitStats.WALQueueWaitDuration, err = d.mu.log.SyncRecord(repr, syncWG, syncErr) + size, err = d.mu.log.SyncRecord(repr, syncWG, syncErr) if err != nil { panic(err) } } - atomic.StoreUint64(&d.atomic.logSize, uint64(size)) + d.logSize.Store(uint64(size)) return mem, err } @@ -947,12 +987,30 @@ var iterAllocPool = sync.Pool{ }, } +// snapshotIterOpts denotes snapshot-related iterator options when calling +// newIter. These are the possible cases for a snapshotIterOpts: +// - No snapshot: All fields are zero values. +// - Classic snapshot: Only `seqNum` is set. The latest readState will be used +// and the specified seqNum will be used as the snapshot seqNum. +// - EventuallyFileOnlySnapshot (EFOS) behaving as a classic snapshot. Only +// the `seqNum` is set. The latest readState will be used +// and the specified seqNum will be used as the snapshot seqNum. +// - EFOS in file-only state: Only `seqNum` and `vers` are set. All the +// relevant SSTs are referenced by the *version. +type snapshotIterOpts struct { + seqNum uint64 + vers *version +} + // newIter constructs a new iterator, merging in batch iterators as an extra // level. -func (d *DB) newIter(ctx context.Context, batch *Batch, s *Snapshot, o *IterOptions) *Iterator { +func (d *DB) newIter( + ctx context.Context, batch *Batch, sOpts snapshotIterOpts, o *IterOptions, +) *Iterator { if err := d.closed.Load(); err != nil { panic(err) } + seqNum := sOpts.seqNum if o.rangeKeys() { if d.FormatMajorVersion() < FormatRangeKeys { panic(fmt.Sprintf( @@ -964,7 +1022,7 @@ func (d *DB) newIter(ctx context.Context, batch *Batch, s *Snapshot, o *IterOpti if o != nil && o.RangeKeyMasking.Suffix != nil && o.KeyTypes != IterKeyTypePointsAndRanges { panic("pebble: range key masking requires IterKeyTypePointsAndRanges") } - if (batch != nil || s != nil) && (o != nil && o.OnlyReadGuaranteedDurable) { + if (batch != nil || seqNum != 0) && (o != nil && o.OnlyReadGuaranteedDurable) { // We could add support for OnlyReadGuaranteedDurable on snapshots if // there was a need: this would require checking that the sequence number // of the snapshot has been flushed, by comparing with @@ -974,15 +1032,19 @@ func (d *DB) newIter(ctx context.Context, batch *Batch, s *Snapshot, o *IterOpti // Grab and reference the current readState. This prevents the underlying // files in the associated version from being deleted if there is a current // compaction. The readState is unref'd by Iterator.Close(). - readState := d.loadReadState() + var readState *readState + if sOpts.vers == nil { + // NB: loadReadState() calls readState.ref(). + readState = d.loadReadState() + } else { + // s.vers != nil + sOpts.vers.Ref() + } // Determine the seqnum to read at after grabbing the read state (current and // memtables) above. - var seqNum uint64 - if s != nil { - seqNum = s.seqNum - } else { - seqNum = atomic.LoadUint64(&d.mu.versions.atomic.visibleSeqNum) + if seqNum == 0 { + seqNum = d.mu.versions.visibleSeqNum.Load() } // Bundle various structures under a single umbrella in order to allocate @@ -995,6 +1057,7 @@ func (d *DB) newIter(ctx context.Context, batch *Batch, s *Snapshot, o *IterOpti merge: d.merge, comparer: *d.opts.Comparer, readState: readState, + version: sOpts.vers, keyBuf: buf.keyBuf, prefixOrFullSeekKey: buf.prefixOrFullSeekKey, boundsBuf: buf.boundsBuf, @@ -1024,7 +1087,10 @@ func (d *DB) newIter(ctx context.Context, batch *Batch, s *Snapshot, o *IterOpti func finishInitializingIter(ctx context.Context, buf *iterAlloc) *Iterator { // Short-hand. dbi := &buf.dbi - memtables := dbi.readState.memtables + var memtables flushableList + if dbi.readState != nil { + memtables = dbi.readState.memtables + } if dbi.opts.OnlyReadGuaranteedDurable { memtables = nil } else { @@ -1086,6 +1152,7 @@ func finishInitializingIter(ctx context.Context, buf *iterAlloc) *Iterator { }, } dbi.iter = &dbi.lazyCombinedIter + dbi.iter = invalidating.MaybeWrapIfInvariants(dbi.iter) } else { dbi.lazyCombinedIter.combinedIterState = combinedIterState{ initialized: true, @@ -1106,7 +1173,11 @@ func finishInitializingIter(ctx context.Context, buf *iterAlloc) *Iterator { // dbi already had an initialized range key iterator, in case the point // iterator changed or the range key masking suffix changed. dbi.rangeKey.iiter.Init(&dbi.comparer, dbi.iter, dbi.rangeKey.rangeKeyIter, - &dbi.rangeKeyMasking, dbi.opts.LowerBound, dbi.opts.UpperBound) + keyspan.InterleavingIterOpts{ + Mask: &dbi.rangeKeyMasking, + LowerBound: dbi.opts.LowerBound, + UpperBound: dbi.opts.UpperBound, + }) dbi.iter = &dbi.rangeKey.iiter } } else { @@ -1128,55 +1199,89 @@ func finishInitializingIter(ctx context.Context, buf *iterAlloc) *Iterator { // method, while the range deletion deleting that key must be exposed using // visitRangeDel. Keys that would be masked by range key masking (if an // appropriate prefix were set) should be exposed, alongside the range key -// that would have masked it. +// that would have masked it. This method also collapses all point keys into +// one InternalKey; so only one internal key at most per user key is returned +// to visitPointKey. +// +// If visitSharedFile is not nil, ScanInternal iterates in skip-shared iteration +// mode. In this iteration mode, sstables in levels L5 and L6 are skipped, and +// their metadatas truncated to [lower, upper) and passed into visitSharedFile. +// ErrInvalidSkipSharedIteration is returned if visitSharedFile is not nil and an +// sstable in L5 or L6 is found that is not in shared storage according to +// provider.IsShared, or an sstable in those levels contains a newer key than the +// snapshot sequence number (only applicable for snapshot.ScanInternal). Examples +// of when this could happen could be if Pebble started writing sstables before a +// creator ID was set (as creator IDs are necessary to enable shared storage) +// resulting in some lower level SSTs being on non-shared storage. Skip-shared +// iteration is invalid in those cases. func (d *DB) ScanInternal( + ctx context.Context, lower, upper []byte, - visitPointKey func(key *InternalKey, value LazyValue) error, + visitPointKey func(key *InternalKey, value LazyValue, iterInfo IteratorLevel) error, visitRangeDel func(start, end []byte, seqNum uint64) error, - visitRangeKey func(start, end []byte, keys []keyspan.Key) error, + visitRangeKey func(start, end []byte, keys []rangekey.Key) error, + visitSharedFile func(sst *SharedSSTMeta) error, ) error { - iter := d.newInternalIter(nil /* snapshot */, &IterOptions{ - KeyTypes: IterKeyTypePointsAndRanges, - LowerBound: lower, - UpperBound: upper, - }) + scanInternalOpts := &scanInternalOptions{ + visitPointKey: visitPointKey, + visitRangeDel: visitRangeDel, + visitRangeKey: visitRangeKey, + visitSharedFile: visitSharedFile, + skipSharedLevels: visitSharedFile != nil, + IterOptions: IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + LowerBound: lower, + UpperBound: upper, + }, + } + iter := d.newInternalIter(snapshotIterOpts{} /* snapshot */, scanInternalOpts) defer iter.close() - return scanInternalImpl(lower, iter, visitPointKey, visitRangeDel, visitRangeKey) + return scanInternalImpl(ctx, lower, upper, iter, scanInternalOpts) } -// NewInternalIter constructs and returns a new scanInternalIterator on this db. +// newInternalIter constructs and returns a new scanInternalIterator on this db. +// If o.skipSharedLevels is true, levels below sharedLevelsStart are *not* added +// to the internal iterator. // // TODO(bilal): This method has a lot of similarities with db.newIter as well as // finishInitializingIter. Both pairs of methods should be refactored to reduce // this duplication. -func (d *DB) newInternalIter(s *Snapshot, o *IterOptions) *scanInternalIterator { +func (d *DB) newInternalIter(sOpts snapshotIterOpts, o *scanInternalOptions) *scanInternalIterator { if err := d.closed.Load(); err != nil { panic(err) } // Grab and reference the current readState. This prevents the underlying // files in the associated version from being deleted if there is a current // compaction. The readState is unref'd by Iterator.Close(). - readState := d.loadReadState() + var readState *readState + if sOpts.vers == nil { + readState = d.loadReadState() + } + if sOpts.vers != nil { + sOpts.vers.Ref() + } // Determine the seqnum to read at after grabbing the read state (current and // memtables) above. - var seqNum uint64 - if s == nil { - seqNum = atomic.LoadUint64(&d.mu.versions.atomic.visibleSeqNum) - } else { - seqNum = s.seqNum + seqNum := sOpts.seqNum + if seqNum == 0 { + seqNum = d.mu.versions.visibleSeqNum.Load() } // Bundle various structures under a single umbrella in order to allocate // them together. buf := iterAllocPool.Get().(*iterAlloc) dbi := &scanInternalIterator{ + db: d, comparer: d.opts.Comparer, + merge: d.opts.Merger.Merge, readState: readState, + version: sOpts.vers, alloc: buf, newIters: d.newIters, newIterRangeKey: d.tableNewRangeKeyIter, seqNum: seqNum, + mergingIter: &buf.merging, } if o != nil { dbi.opts = *o @@ -1190,7 +1295,10 @@ func (d *DB) newInternalIter(s *Snapshot, o *IterOptions) *scanInternalIterator func finishInitializingInternalIter(buf *iterAlloc, i *scanInternalIterator) *scanInternalIterator { // Short-hand. - memtables := i.readState.memtables + var memtables flushableList + if i.readState != nil { + memtables = i.readState.memtables + } // We only need to read from memtables which contain sequence numbers older // than seqNum. Trim off newer memtables. for j := len(memtables) - 1; j >= 0; j-- { @@ -1205,19 +1313,18 @@ func finishInitializingInternalIter(buf *iterAlloc, i *scanInternalIterator) *sc // For internal iterators, we skip the lazy combined iteration optimization // entirely, and create the range key iterator stack directly. - if i.rangeKey == nil { - i.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState) - i.rangeKey.init(i.comparer.Compare, i.comparer.Split, &i.opts) - i.constructRangeKeyIter() - } else { - i.rangeKey.iterConfig.SetBounds(i.opts.LowerBound, i.opts.UpperBound) - } + i.rangeKey = iterRangeKeyStateAllocPool.Get().(*iteratorRangeKeyState) + i.rangeKey.init(i.comparer.Compare, i.comparer.Split, &i.opts.IterOptions) + i.constructRangeKeyIter() // Wrap the point iterator (currently i.iter) with an interleaving // iterator that interleaves range keys pulled from // i.rangeKey.rangeKeyIter. i.rangeKey.iiter.Init(i.comparer, i.iter, i.rangeKey.rangeKeyIter, - nil /* mask */, i.opts.LowerBound, i.opts.UpperBound) + keyspan.InterleavingIterOpts{ + LowerBound: i.opts.LowerBound, + UpperBound: i.opts.UpperBound, + }) i.iter = &i.rangeKey.iiter return i @@ -1249,7 +1356,10 @@ func (i *Iterator) constructPointIter( } numMergingLevels += len(memtables) - current := i.readState.current + current := i.version + if current == nil { + current = i.readState.current + } numMergingLevels += len(current.L0SublevelFiles) numLevelIters += len(current.L0SublevelFiles) for level := 1; level < len(current.Levels); level++ { @@ -1309,15 +1419,16 @@ func (i *Iterator) constructPointIter( levelsIndex := len(levels) mlevels = mlevels[:numMergingLevels] levels = levels[:numLevelIters] + i.opts.snapshotForHideObsoletePoints = buf.dbi.seqNum addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) { li := &levels[levelsIndex] - li.init( - ctx, i.opts, i.comparer.Compare, i.comparer.Split, i.newIters, files, level, internalOpts) + li.init(ctx, i.opts, &i.comparer, i.newIters, files, level, internalOpts) li.initRangeDel(&mlevels[mlevelsIndex].rangeDelIter) li.initBoundaryContext(&mlevels[mlevelsIndex].levelIterBoundaryContext) li.initCombinedIterState(&i.lazyCombinedIter.combinedIterState) - mlevels[mlevelsIndex].iter = li + mlevels[mlevelsIndex].levelIter = li + mlevels[mlevelsIndex].iter = invalidating.MaybeWrapIfInvariants(li) levelsIndex++ mlevelsIndex++ @@ -1343,7 +1454,7 @@ func (i *Iterator) constructPointIter( buf.merging.snapshot = i.seqNum buf.merging.batchSnapshot = i.batchSeqNum buf.merging.combinedIterState = &i.lazyCombinedIter.combinedIterState - i.pointIter = &buf.merging + i.pointIter = invalidating.MaybeWrapIfInvariants(&buf.merging) i.merging = &buf.merging } @@ -1353,6 +1464,12 @@ func (d *DB) NewBatch() *Batch { return newBatch(d) } +// NewBatchWithSize is mostly identical to NewBatch, but it will allocate the +// the specified memory space for the internal slice in advance. +func (d *DB) NewBatchWithSize(size int) *Batch { + return newBatchWithSize(d, size) +} + // NewIndexedBatch returns a new empty read-write batch. Any reads on the batch // will read from both the batch and the DB. If the batch is committed it will // be applied to the DB. An indexed batch is slower that a non-indexed batch @@ -1362,6 +1479,12 @@ func (d *DB) NewIndexedBatch() *Batch { return newIndexedBatch(d, d.opts.Comparer) } +// NewIndexedBatchWithSize is mostly identical to NewIndexedBatch, but it will +// allocate the the specified memory space for the internal slice in advance. +func (d *DB) NewIndexedBatchWithSize(size int) *Batch { + return newIndexedBatchWithSize(d, d.opts.Comparer, size) +} + // NewIter returns an iterator that is unpositioned (Iterator.Valid() will // return false). The iterator can be positioned via a call to SeekGE, SeekLT, // First or Last. The iterator provides a point-in-time view of the current DB @@ -1370,14 +1493,14 @@ func (d *DB) NewIndexedBatch() *Batch { // to maintain a long-lived point-in-time view of the DB state can lead to an // apparent memory and disk usage leak. Use snapshots (see NewSnapshot) for // point-in-time snapshots which avoids these problems. -func (d *DB) NewIter(o *IterOptions) *Iterator { +func (d *DB) NewIter(o *IterOptions) (*Iterator, error) { return d.NewIterWithContext(context.Background(), o) } // NewIterWithContext is like NewIter, and additionally accepts a context for // tracing. -func (d *DB) NewIterWithContext(ctx context.Context, o *IterOptions) *Iterator { - return d.newIter(ctx, nil /* batch */, nil /* snapshot */, o) +func (d *DB) NewIterWithContext(ctx context.Context, o *IterOptions) (*Iterator, error) { + return d.newIter(ctx, nil /* batch */, snapshotIterOpts{}, o), nil } // NewSnapshot returns a point-in-time view of the current DB state. Iterators @@ -1396,13 +1519,36 @@ func (d *DB) NewSnapshot() *Snapshot { d.mu.Lock() s := &Snapshot{ db: d, - seqNum: atomic.LoadUint64(&d.mu.versions.atomic.visibleSeqNum), + seqNum: d.mu.versions.visibleSeqNum.Load(), } d.mu.snapshots.pushBack(s) d.mu.Unlock() return s } +// NewEventuallyFileOnlySnapshot returns a point-in-time view of the current DB +// state, similar to NewSnapshot, but with consistency constrained to the +// provided set of key ranges. See the comment at EventuallyFileOnlySnapshot for +// its semantics. +func (d *DB) NewEventuallyFileOnlySnapshot(keyRanges []KeyRange) *EventuallyFileOnlySnapshot { + if err := d.closed.Load(); err != nil { + panic(err) + } + + internalKeyRanges := make([]internalKeyRange, len(keyRanges)) + for i := range keyRanges { + if i > 0 && d.cmp(keyRanges[i-1].End, keyRanges[i].Start) > 0 { + panic("pebble: key ranges for eventually-file-only-snapshot not in order") + } + internalKeyRanges[i] = internalKeyRange{ + smallest: base.MakeInternalKey(keyRanges[i].Start, InternalKeySeqNumMax, InternalKeyKindMax), + largest: base.MakeExclusiveSentinelKey(InternalKeyKindRangeDelete, keyRanges[i].End), + } + } + + return d.makeEventuallyFileOnlySnapshot(keyRanges, internalKeyRanges) +} + // Close closes the DB. // // It is not safe to close a DB until all outstanding iterators are closed @@ -1494,25 +1640,26 @@ func (d *DB) Close() error { // replay. mem.readerUnrefLocked(false) } - if reserved := atomic.LoadInt64(&d.atomic.memTableReserved); reserved != 0 { + // If there's an unused, recycled memtable, we need to release its memory. + if obsoleteMemTable := d.memTableRecycle.Swap(nil); obsoleteMemTable != nil { + d.freeMemTable(obsoleteMemTable) + } + if reserved := d.memTableReserved.Load(); reserved != 0 { err = firstError(err, errors.Errorf("leaked memtable reservation: %d", errors.Safe(reserved))) } - // No more cleaning can start. Wait for any async cleaning to complete. - for d.mu.cleaner.cleaning { - d.mu.cleaner.cond.Wait() - } - // There may still be obsolete tables if an existing async cleaning job - // prevented a new cleaning job when a readState was unrefed. If needed, - // synchronously delete obsolete files. + // Since we called d.readState.val.unrefLocked() above, we are expected to + // manually schedule deletion of obsolete files. if len(d.mu.versions.obsoleteTables) > 0 { - d.deleteObsoleteFiles(d.mu.nextJobID, true /* waitForOngoing */) + d.deleteObsoleteFiles(d.mu.nextJobID) } - // Wait for all the deletion goroutines spawned by cleaning jobs to finish. + d.mu.Unlock() - d.deleters.Wait() d.compactionSchedulers.Wait() + // Wait for all cleaning jobs to finish. + d.cleanupManager.Close() + // Sanity check metrics. if invariants.Enabled { m := d.Metrics() @@ -1572,6 +1719,10 @@ func (d *DB) Compact(start, end []byte, parallelize bool) error { } } + keyRanges := make([]internalKeyRange, len(meta)) + for i := range meta { + keyRanges[i] = internalKeyRange{smallest: m.Smallest, largest: m.Largest} + } // Determine if any memtable overlaps with the compaction range. We wait for // any such overlap to flush (initiating a flush if necessary). mem, err := func() (*flushableEntry, error) { @@ -1581,7 +1732,7 @@ func (d *DB) Compact(start, end []byte, parallelize bool) error { // overlaps. for i := len(d.mu.mem.queue) - 1; i >= 0; i-- { mem := d.mu.mem.queue[i] - if ingestMemtableOverlaps(d.cmp, mem, meta) { + if ingestMemtableOverlaps(d.cmp, mem, keyRanges) { var err error if mem.flushable == d.mu.mem.mutable { // We have to hold both commitPipeline.mu and DB.mu when calling @@ -1691,6 +1842,30 @@ func (d *DB) splitManualCompaction( return splitCompactions } +// DownloadSpan is a key range passed to the Download method. +type DownloadSpan struct { + StartKey []byte + // EndKey is exclusive. + EndKey []byte +} + +// Download ensures that the LSM does not use any external sstables for the +// given key ranges. It does so by performing appropriate compactions so that +// all external data becomes available locally. +// +// Note that calling this method does not imply that all other compactions stop; +// it simply informs Pebble of a list of spans for which external data should be +// downloaded with high priority. +// +// The method returns once no external sstasbles overlap the given spans, the +// context is canceled, or an error is hit. +// +// TODO(radu): consider passing a priority/impact knob to express how important +// the download is (versus live traffic performance, LSM health). +func (d *DB) Download(ctx context.Context, spans []DownloadSpan) error { + return errors.Errorf("not implemented") +} + // Flush the memtable to stable storage. func (d *DB) Flush() error { flushDone, err := d.AsyncFlush() @@ -1734,9 +1909,16 @@ func (d *DB) Metrics() *Metrics { vers := d.mu.versions.currentVersion() *metrics = d.mu.versions.metrics metrics.Compact.EstimatedDebt = d.mu.versions.picker.estimatedCompactionDebt(0) - metrics.Compact.InProgressBytes = atomic.LoadInt64(&d.mu.versions.atomic.atomicInProgressBytes) + metrics.Compact.InProgressBytes = d.mu.versions.atomicInProgressBytes.Load() metrics.Compact.NumInProgress = int64(d.mu.compact.compactingCount) metrics.Compact.MarkedFiles = vers.Stats.MarkedForCompaction + metrics.Compact.Duration = d.mu.compact.duration + for c := range d.mu.compact.inProgress { + if c.kind != compactionKindFlush { + metrics.Compact.Duration += d.timeNow().Sub(c.beganAt) + } + } + for _, m := range d.mu.mem.queue { metrics.MemTable.Size += m.totalBytes() } @@ -1747,11 +1929,11 @@ func (d *DB) Metrics() *Metrics { metrics.Snapshots.PinnedKeys = d.mu.snapshots.cumulativePinnedCount metrics.Snapshots.PinnedSize = d.mu.snapshots.cumulativePinnedSize metrics.MemTable.Count = int64(len(d.mu.mem.queue)) - metrics.MemTable.ZombieCount = atomic.LoadInt64(&d.atomic.memTableCount) - metrics.MemTable.Count - metrics.MemTable.ZombieSize = uint64(atomic.LoadInt64(&d.atomic.memTableReserved)) - metrics.MemTable.Size + metrics.MemTable.ZombieCount = d.memTableCount.Load() - metrics.MemTable.Count + metrics.MemTable.ZombieSize = uint64(d.memTableReserved.Load()) - metrics.MemTable.Size metrics.WAL.ObsoleteFiles = int64(recycledLogsCount) metrics.WAL.ObsoletePhysicalSize = recycledLogSize - metrics.WAL.Size = atomic.LoadUint64(&d.atomic.logSize) + metrics.WAL.Size = d.logSize.Load() // The current WAL size (d.atomic.logSize) is the current logical size, // which may be less than the WAL's physical size if it was recycled. // The file sizes in d.mu.log.queue are updated to the physical size @@ -1788,6 +1970,17 @@ func (d *DB) Metrics() *Metrics { d.mu.versions.logLock() metrics.private.manifestFileSize = uint64(d.mu.versions.manifest.Size()) + metrics.Table.BackingTableCount = uint64(len(d.mu.versions.backingState.fileBackingMap)) + metrics.Table.BackingTableSize = d.mu.versions.backingState.fileBackingSize + if invariants.Enabled { + var totalSize uint64 + for _, backing := range d.mu.versions.backingState.fileBackingMap { + totalSize += backing.Size + } + if totalSize != metrics.Table.BackingTableSize { + panic("pebble: invalid backing table size accounting") + } + } d.mu.versions.logUnlock() metrics.LogWriter.FsyncLatency = d.mu.log.metrics.fsyncLatency @@ -1807,6 +2000,11 @@ func (d *DB) Metrics() *Metrics { metrics.BlockCache = d.opts.Cache.Metrics() metrics.TableCache, metrics.Filter = d.tableCache.metrics() metrics.TableIters = int64(d.tableCache.iterCount()) + + metrics.SecondaryCacheMetrics = d.objProvider.Metrics() + + metrics.Uptime = d.timeNow().Sub(d.openedAt) + return metrics } @@ -1814,6 +2012,12 @@ func (d *DB) Metrics() *Metrics { type sstablesOptions struct { // set to true will return the sstable properties in TableInfo withProperties bool + + // if set, return sstables that overlap the key range (end-exclusive) + start []byte + end []byte + + withApproximateSpanBytes bool } // SSTablesOption set optional parameter used by `DB.SSTables`. @@ -1829,11 +2033,65 @@ func WithProperties() SSTablesOption { } } -// SSTableInfo export manifest.TableInfo with sstable.Properties +// WithKeyRangeFilter ensures returned sstables overlap start and end (end-exclusive) +// if start and end are both nil these properties have no effect. +func WithKeyRangeFilter(start, end []byte) SSTablesOption { + return func(opt *sstablesOptions) { + opt.end = end + opt.start = start + } +} + +// WithApproximateSpanBytes enables capturing the approximate number of bytes that +// overlap the provided key span for each sstable. +// NOTE: this option can only be used with WithKeyRangeFilter and WithProperties +// provided. +func WithApproximateSpanBytes() SSTablesOption { + return func(opt *sstablesOptions) { + opt.withApproximateSpanBytes = true + } +} + +// BackingType denotes the type of storage backing a given sstable. +type BackingType int + +const ( + // BackingTypeLocal denotes an sstable stored on local disk according to the + // objprovider. This file is completely owned by us. + BackingTypeLocal BackingType = iota + // BackingTypeShared denotes an sstable stored on shared storage, created + // by this Pebble instance and possibly shared by other Pebble instances. + // These types of files have lifecycle managed by Pebble. + BackingTypeShared + // BackingTypeSharedForeign denotes an sstable stored on shared storage, + // created by a Pebble instance other than this one. These types of files have + // lifecycle managed by Pebble. + BackingTypeSharedForeign + // BackingTypeExternal denotes an sstable stored on external storage, + // not owned by any Pebble instance and with no refcounting/cleanup methods + // or lifecycle management. An example of an external file is a file restored + // from a backup. + BackingTypeExternal +) + +// SSTableInfo export manifest.TableInfo with sstable.Properties alongside +// other file backing info. type SSTableInfo struct { manifest.TableInfo + // Virtual indicates whether the sstable is virtual. + Virtual bool + // BackingSSTNum is the file number associated with backing sstable which + // backs the sstable associated with this SSTableInfo. If Virtual is false, + // then BackingSSTNum == FileNum. + BackingSSTNum base.FileNum + // BackingType is the type of storage backing this sstable. + BackingType BackingType + // Locator is the remote.Locator backing this sstable, if the backing type is + // not BackingTypeLocal. + Locator remote.Locator - // Properties is the sstable properties of this table. + // Properties is the sstable properties of this table. If Virtual is true, + // then the Properties are associated with the backing sst. Properties *sstable.Properties } @@ -1847,6 +2105,13 @@ func (d *DB) SSTables(opts ...SSTablesOption) ([][]SSTableInfo, error) { fn(opt) } + if opt.withApproximateSpanBytes && !opt.withProperties { + return nil, errors.Errorf("Cannot use WithApproximateSpanBytes without WithProperties option.") + } + if opt.withApproximateSpanBytes && (opt.start == nil || opt.end == nil) { + return nil, errors.Errorf("Cannot use WithApproximateSpanBytes without WithKeyRangeFilter option.") + } + // Grab and reference the current readState. readState := d.loadReadState() defer readState.unref() @@ -1867,19 +2132,67 @@ func (d *DB) SSTables(opts ...SSTablesOption) ([][]SSTableInfo, error) { iter := srcLevels[i].Iter() j := 0 for m := iter.First(); m != nil; m = iter.Next() { + if opt.start != nil && opt.end != nil && !m.Overlaps(d.opts.Comparer.Compare, opt.start, opt.end, true /* exclusive end */) { + continue + } destTables[j] = SSTableInfo{TableInfo: m.TableInfo()} if opt.withProperties { - p, err := d.tableCache.getTableProperties(m) + p, err := d.tableCache.getTableProperties( + m, + ) if err != nil { return nil, err } destTables[j].Properties = p } + destTables[j].Virtual = m.Virtual + destTables[j].BackingSSTNum = m.FileBacking.DiskFileNum.FileNum() + objMeta, err := d.objProvider.Lookup(fileTypeTable, m.FileBacking.DiskFileNum) + if err != nil { + return nil, err + } + if objMeta.IsRemote() { + if objMeta.IsShared() { + if d.objProvider.IsSharedForeign(objMeta) { + destTables[j].BackingType = BackingTypeSharedForeign + } else { + destTables[j].BackingType = BackingTypeShared + } + } else { + destTables[j].BackingType = BackingTypeExternal + } + destTables[j].Locator = objMeta.Remote.Locator + } else { + destTables[j].BackingType = BackingTypeLocal + } + + if opt.withApproximateSpanBytes { + var spanBytes uint64 + if m.ContainedWithinSpan(d.opts.Comparer.Compare, opt.start, opt.end) { + spanBytes = m.Size + } else { + size, err := d.tableCache.estimateSize(m, opt.start, opt.end) + if err != nil { + return nil, err + } + spanBytes = size + } + propertiesCopy := *destTables[j].Properties + + // Deep copy user properties so approximate span bytes can be added. + propertiesCopy.UserProperties = make(map[string]string, len(destTables[j].Properties.UserProperties)+1) + for k, v := range destTables[j].Properties.UserProperties { + propertiesCopy.UserProperties[k] = v + } + propertiesCopy.UserProperties["approximate-span-bytes"] = strconv.FormatUint(spanBytes, 10) + destTables[j].Properties = &propertiesCopy + } j++ } destLevels[i] = destTables[:j] destTables = destTables[j:] } + return destLevels, nil } @@ -1892,14 +2205,25 @@ func (d *DB) SSTables(opts ...SSTablesOption) ([][]SSTableInfo, error) { // overlap due to abbreviated index keys, the full data block size is included in // the estimation. Note that unlike fully contained sstables, none of the // meta-block space is counted for partially overlapped files. +// - For virtual sstables, we use the overlap between start, end and the virtual +// sstable bounds to determine disk usage. // - There may also exist WAL entries for unflushed keys in this range. This // estimation currently excludes space used for the range in the WAL. func (d *DB) EstimateDiskUsage(start, end []byte) (uint64, error) { + bytes, _, _, err := d.EstimateDiskUsageByBackingType(start, end) + return bytes, err +} + +// EstimateDiskUsageByBackingType is like EstimateDiskUsage but additionally +// returns the subsets of that size in remote ane external files. +func (d *DB) EstimateDiskUsageByBackingType( + start, end []byte, +) (totalSize, remoteSize, externalSize uint64, _ error) { if err := d.closed.Load(); err != nil { panic(err) } if d.opts.Comparer.Compare(start, end) > 0 { - return 0, errors.New("invalid key-range specified (start > end)") + return 0, 0, 0, errors.New("invalid key-range specified (start > end)") } // Grab and reference the current readState. This prevents the underlying @@ -1908,7 +2232,6 @@ func (d *DB) EstimateDiskUsage(start, end []byte) (uint64, error) { readState := d.loadReadState() defer readState.unref() - var totalSize uint64 for level, files := range readState.current.Levels { iter := files.Iter() if level > 0 { @@ -1923,22 +2246,56 @@ func (d *DB) EstimateDiskUsage(start, end []byte) (uint64, error) { d.opts.Comparer.Compare(file.Largest.UserKey, end) <= 0 { // The range fully contains the file, so skip looking it up in // table cache/looking at its indexes, and add the full file size. + meta, err := d.objProvider.Lookup(fileTypeTable, file.FileBacking.DiskFileNum) + if err != nil { + return 0, 0, 0, err + } + if meta.IsRemote() { + remoteSize += file.Size + if meta.Remote.CleanupMethod == objstorage.SharedNoCleanup { + externalSize += file.Size + } + } totalSize += file.Size } else if d.opts.Comparer.Compare(file.Smallest.UserKey, end) <= 0 && d.opts.Comparer.Compare(start, file.Largest.UserKey) <= 0 { var size uint64 - err := d.tableCache.withReader(file, func(r *sstable.Reader) (err error) { - size, err = r.EstimateDiskUsage(start, end) - return err - }) + var err error + if file.Virtual { + err = d.tableCache.withVirtualReader( + file.VirtualMeta(), + func(r sstable.VirtualReader) (err error) { + size, err = r.EstimateDiskUsage(start, end) + return err + }, + ) + } else { + err = d.tableCache.withReader( + file.PhysicalMeta(), + func(r *sstable.Reader) (err error) { + size, err = r.EstimateDiskUsage(start, end) + return err + }, + ) + } if err != nil { - return 0, err + return 0, 0, 0, err + } + meta, err := d.objProvider.Lookup(fileTypeTable, file.FileBacking.DiskFileNum) + if err != nil { + return 0, 0, 0, err + } + if meta.IsRemote() { + remoteSize += size + if meta.Remote.CleanupMethod == objstorage.SharedNoCleanup { + externalSize += size + } } totalSize += size } } } - return totalSize, nil + return totalSize, remoteSize, externalSize, nil } func (d *DB) walPreallocateSize() int { @@ -1952,7 +2309,7 @@ func (d *DB) walPreallocateSize() int { // RocksDB. Could a smaller preallocation block size be used? size := d.opts.MemTableSize size = (size / 10) + size - return size + return int(size) } func (d *DB) newMemTable(logNum FileNum, logSeqNum uint64) (*memTable, *flushableEntry) { @@ -1964,40 +2321,79 @@ func (d *DB) newMemTable(logNum FileNum, logSeqNum uint64) (*memTable, *flushabl } } - atomic.AddInt64(&d.atomic.memTableCount, 1) - atomic.AddInt64(&d.atomic.memTableReserved, int64(size)) - releaseAccountingReservation := d.opts.Cache.Reserve(size) - - mem := newMemTable(memTableOptions{ + memtblOpts := memTableOptions{ Options: d.opts, - arenaBuf: manual.New(int(size)), logSeqNum: logSeqNum, - }) + } - // Note: this is a no-op if invariants are disabled or race is enabled. - invariants.SetFinalizer(mem, checkMemTable) + // Before attempting to allocate a new memtable, check if there's one + // available for recycling in memTableRecycle. Large contiguous allocations + // can be costly as fragmentation makes it more difficult to find a large + // contiguous free space. We've observed 64MB allocations taking 10ms+. + // + // To reduce these costly allocations, up to 1 obsolete memtable is stashed + // in `d.memTableRecycle` to allow a future memtable rotation to reuse + // existing memory. + var mem *memTable + mem = d.memTableRecycle.Swap(nil) + if mem != nil && uint64(len(mem.arenaBuf)) != size { + d.freeMemTable(mem) + mem = nil + } + if mem != nil { + // Carry through the existing buffer and memory reservation. + memtblOpts.arenaBuf = mem.arenaBuf + memtblOpts.releaseAccountingReservation = mem.releaseAccountingReservation + } else { + mem = new(memTable) + memtblOpts.arenaBuf = manual.New(int(size)) + memtblOpts.releaseAccountingReservation = d.opts.Cache.Reserve(int(size)) + d.memTableCount.Add(1) + d.memTableReserved.Add(int64(size)) + + // Note: this is a no-op if invariants are disabled or race is enabled. + invariants.SetFinalizer(mem, checkMemTable) + } + mem.init(memtblOpts) entry := d.newFlushableEntry(mem, logNum, logSeqNum) entry.releaseMemAccounting = func() { - manual.Free(mem.arenaBuf) - mem.arenaBuf = nil - atomic.AddInt64(&d.atomic.memTableCount, -1) - atomic.AddInt64(&d.atomic.memTableReserved, -int64(size)) - releaseAccountingReservation() + // If the user leaks iterators, we may be releasing the memtable after + // the DB is already closed. In this case, we want to just release the + // memory because DB.Close won't come along to free it for us. + if err := d.closed.Load(); err != nil { + d.freeMemTable(mem) + return + } + + // The next memtable allocation might be able to reuse this memtable. + // Stash it on d.memTableRecycle. + if unusedMem := d.memTableRecycle.Swap(mem); unusedMem != nil { + // There was already a memtable waiting to be recycled. We're now + // responsible for freeing it. + d.freeMemTable(unusedMem) + } } return mem, entry } +func (d *DB) freeMemTable(m *memTable) { + d.memTableCount.Add(-1) + d.memTableReserved.Add(-int64(len(m.arenaBuf))) + m.free() +} + func (d *DB) newFlushableEntry(f flushable, logNum FileNum, logSeqNum uint64) *flushableEntry { - return &flushableEntry{ + fe := &flushableEntry{ flushable: f, flushed: make(chan struct{}), logNum: logNum, logSeqNum: logSeqNum, - readerRefs: 1, deleteFn: d.mu.versions.addObsolete, deleteFnLocked: d.mu.versions.addObsoleteLocked, } + fe.readerRefs.Store(1) + return fe } // makeRoomForWrite ensures that the memtable has room to hold the contents of @@ -2037,7 +2433,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error { for i := range d.mu.mem.queue { size += d.mu.mem.queue[i].totalBytes() } - if size >= uint64(d.opts.MemTableStopWritesThreshold)*uint64(d.opts.MemTableSize) { + if size >= uint64(d.opts.MemTableStopWritesThreshold)*d.opts.MemTableSize { // We have filled up the current memtable, but already queued memtables // are still flushing, so we wait. if !stalled { @@ -2091,7 +2487,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error { // reduces memtable memory pressure when an application is frequently // manually flushing. if (b == nil) && uint64(immMem.availBytes()) > immMem.totalBytes()/2 { - d.mu.mem.nextSize = int(immMem.totalBytes()) + d.mu.mem.nextSize = immMem.totalBytes() } if b != nil && b.flushable != nil { @@ -2120,7 +2516,7 @@ func (d *DB) makeRoomForWrite(b *Batch) error { logSeqNum += uint64(b.Count()) } } else { - logSeqNum = atomic.LoadUint64(&d.mu.versions.atomic.logSeqNum) + logSeqNum = d.mu.versions.logSeqNum.Load() } d.rotateMemtable(newLogNum, logSeqNum, immMem) force = false @@ -2188,7 +2584,7 @@ func (d *DB) recycleWAL() (newLogNum FileNum, prevLogSize uint64) { } d.mu.Unlock() - newLogName := base.MakeFilepath(d.opts.FS, d.walDirname, fileTypeLog, newLogNum) + newLogName := base.MakeFilepath(d.opts.FS, d.walDirname, fileTypeLog, newLogNum.DiskFileNum()) // Try to use a recycled log file. Recycling log files is an important // performance optimization as it is faster to sync a file that has @@ -2245,19 +2641,18 @@ func (d *DB) recycleWAL() (newLogNum FileNum, prevLogSize uint64) { } if recycleOK { - err = firstError(err, d.logRecycler.pop(recycleLog.fileNum)) + err = firstError(err, d.logRecycler.pop(recycleLog.fileNum.FileNum())) } d.opts.EventListener.WALCreated(WALCreateInfo{ JobID: jobID, Path: newLogName, FileNum: newLogNum, - RecycledFileNum: recycleLog.fileNum, + RecycledFileNum: recycleLog.fileNum.FileNum(), Err: err, }) d.mu.Lock() - d.mu.mem.cond.Broadcast() d.mu.versions.metrics.WAL.Files++ @@ -2270,7 +2665,7 @@ func (d *DB) recycleWAL() (newLogNum FileNum, prevLogSize uint64) { panic(err) } - d.mu.log.queue = append(d.mu.log.queue, fileInfo{fileNum: newLogNum, fileSize: newLogSize}) + d.mu.log.queue = append(d.mu.log.queue, fileInfo{fileNum: newLogNum.DiskFileNum(), fileSize: newLogSize}) d.mu.log.LogWriter = record.NewLogWriter(newLogFile, newLogNum, record.LogWriterConfig{ WALFsyncLatency: d.mu.log.metrics.fsyncLatency, WALMinSyncInterval: d.opts.WALMinSyncInterval, @@ -2298,10 +2693,11 @@ func (d *DB) getInProgressCompactionInfoLocked(finishing *compaction) (rv []comp for c := range d.mu.compact.inProgress { if len(c.flushing) == 0 && (finishing == nil || c != finishing) { info := compactionInfo{ - inputs: c.inputs, - smallest: c.smallest, - largest: c.largest, - outputLevel: -1, + versionEditApplied: c.versionEditApplied, + inputs: c.inputs, + smallest: c.smallest, + largest: c.largest, + outputLevel: -1, } if c.outputLevel != nil { info.outputLevel = c.outputLevel.level @@ -2315,6 +2711,14 @@ func (d *DB) getInProgressCompactionInfoLocked(finishing *compaction) (rv []comp func inProgressL0Compactions(inProgress []compactionInfo) []manifest.L0Compaction { var compactions []manifest.L0Compaction for _, info := range inProgress { + // Skip in-progress compactions that have already committed; the L0 + // sublevels initialization code requires the set of in-progress + // compactions to be consistent with the current version. Compactions + // with versionEditApplied=true are already applied to the current + // version and but are performing cleanup without the database mutex. + if info.versionEditApplied { + continue + } l0 := false for _, cl := range info.inputs { l0 = l0 || cl.level == 0 @@ -2341,14 +2745,246 @@ func firstError(err0, err1 error) error { } // SetCreatorID sets the CreatorID which is needed in order to use shared objects. -// Shared object usage is disabled until this method is called the first time. +// Remote object usage is disabled until this method is called the first time. // Once set, the Creator ID is persisted and cannot change. // // Does nothing if SharedStorage was not set in the options when the DB was // opened or if the DB is in read-only mode. func (d *DB) SetCreatorID(creatorID uint64) error { - if d.opts.Experimental.SharedStorage == nil || d.opts.ReadOnly { + if d.opts.Experimental.RemoteStorage == nil || d.opts.ReadOnly { return nil } return d.objProvider.SetCreatorID(objstorage.CreatorID(creatorID)) } + +// KeyStatistics keeps track of the number of keys that have been pinned by a +// snapshot as well as counts of the different key kinds in the lsm. +// +// One way of using the accumulated stats, when we only have sets and dels, +// and say the counts are represented as del_count, set_count, +// del_latest_count, set_latest_count, snapshot_pinned_count. +// +// - del_latest_count + set_latest_count is the set of unique user keys +// (unique). +// +// - set_latest_count is the set of live unique user keys (live_unique). +// +// - Garbage is del_count + set_count - live_unique. +// +// - If everything were in the LSM, del_count+set_count-snapshot_pinned_count +// would also be the set of unique user keys (note that +// snapshot_pinned_count is counting something different -- see comment below). +// But snapshot_pinned_count only counts keys in the LSM so the excess here +// must be keys in memtables. +type KeyStatistics struct { + // TODO(sumeer): the SnapshotPinned* are incorrect in that these older + // versions can be in a different level. Either fix the accounting or + // rename these fields. + + // SnapshotPinnedKeys represents obsolete keys that cannot be elided during + // a compaction, because they are required by an open snapshot. + SnapshotPinnedKeys int + // SnapshotPinnedKeysBytes is the total number of bytes of all snapshot + // pinned keys. + SnapshotPinnedKeysBytes uint64 + // KindsCount is the count for each kind of key. It includes point keys, + // range deletes and range keys. + KindsCount [InternalKeyKindMax + 1]int + // LatestKindsCount is the count for each kind of key when it is the latest + // kind for a user key. It is only populated for point keys. + LatestKindsCount [InternalKeyKindMax + 1]int +} + +// LSMKeyStatistics is used by DB.ScanStatistics. +type LSMKeyStatistics struct { + Accumulated KeyStatistics + // Levels contains statistics only for point keys. Range deletions and range keys will + // appear in Accumulated but not Levels. + Levels [numLevels]KeyStatistics + // BytesRead represents the logical, pre-compression size of keys and values read + BytesRead uint64 +} + +// ScanStatisticsOptions is used by DB.ScanStatistics. +type ScanStatisticsOptions struct { + // LimitBytesPerSecond indicates the number of bytes that are able to be read + // per second using ScanInternal. + // A value of 0 indicates that there is no limit set. + LimitBytesPerSecond int64 +} + +// ScanStatistics returns the count of different key kinds within the lsm for a +// key span [lower, upper) as well as the number of snapshot keys. +func (d *DB) ScanStatistics( + ctx context.Context, lower, upper []byte, opts ScanStatisticsOptions, +) (LSMKeyStatistics, error) { + stats := LSMKeyStatistics{} + var prevKey InternalKey + var rateLimitFunc func(key *InternalKey, val LazyValue) error + tb := tokenbucket.TokenBucket{} + + if opts.LimitBytesPerSecond != 0 { + // Each "token" roughly corresponds to a byte that was read. + tb.Init(tokenbucket.TokensPerSecond(opts.LimitBytesPerSecond), tokenbucket.Tokens(1024)) + rateLimitFunc = func(key *InternalKey, val LazyValue) error { + return tb.WaitCtx(ctx, tokenbucket.Tokens(key.Size()+val.Len())) + } + } + + scanInternalOpts := &scanInternalOptions{ + visitPointKey: func(key *InternalKey, value LazyValue, iterInfo IteratorLevel) error { + // If the previous key is equal to the current point key, the current key was + // pinned by a snapshot. + size := uint64(key.Size()) + kind := key.Kind() + sameKey := d.equal(prevKey.UserKey, key.UserKey) + if iterInfo.Kind == IteratorLevelLSM && sameKey { + stats.Levels[iterInfo.Level].SnapshotPinnedKeys++ + stats.Levels[iterInfo.Level].SnapshotPinnedKeysBytes += size + stats.Accumulated.SnapshotPinnedKeys++ + stats.Accumulated.SnapshotPinnedKeysBytes += size + } + if iterInfo.Kind == IteratorLevelLSM { + stats.Levels[iterInfo.Level].KindsCount[kind]++ + } + if !sameKey { + if iterInfo.Kind == IteratorLevelLSM { + stats.Levels[iterInfo.Level].LatestKindsCount[kind]++ + } + stats.Accumulated.LatestKindsCount[kind]++ + } + + stats.Accumulated.KindsCount[kind]++ + prevKey.CopyFrom(*key) + stats.BytesRead += uint64(key.Size() + value.Len()) + return nil + }, + visitRangeDel: func(start, end []byte, seqNum uint64) error { + stats.Accumulated.KindsCount[InternalKeyKindRangeDelete]++ + stats.BytesRead += uint64(len(start) + len(end)) + return nil + }, + visitRangeKey: func(start, end []byte, keys []rangekey.Key) error { + stats.BytesRead += uint64(len(start) + len(end)) + for _, key := range keys { + stats.Accumulated.KindsCount[key.Kind()]++ + stats.BytesRead += uint64(len(key.Value) + len(key.Suffix)) + } + return nil + }, + includeObsoleteKeys: true, + IterOptions: IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + LowerBound: lower, + UpperBound: upper, + }, + rateLimitFunc: rateLimitFunc, + } + iter := d.newInternalIter(snapshotIterOpts{}, scanInternalOpts) + defer iter.close() + + err := scanInternalImpl(ctx, lower, upper, iter, scanInternalOpts) + + if err != nil { + return LSMKeyStatistics{}, err + } + + return stats, nil +} + +// ObjProvider returns the objstorage.Provider for this database. Meant to be +// used for internal purposes only. +func (d *DB) ObjProvider() objstorage.Provider { + return d.objProvider +} + +func (d *DB) checkVirtualBounds(m *fileMetadata) { + if !invariants.Enabled { + return + } + + if m.HasPointKeys { + pointIter, rangeDelIter, err := d.newIters(context.TODO(), m, nil, internalIterOpts{}) + if err != nil { + panic(errors.Wrap(err, "pebble: error creating point iterator")) + } + + defer pointIter.Close() + if rangeDelIter != nil { + defer rangeDelIter.Close() + } + + pointKey, _ := pointIter.First() + var rangeDel *keyspan.Span + if rangeDelIter != nil { + rangeDel = rangeDelIter.First() + } + + // Check that the lower bound is tight. + if (rangeDel == nil || d.cmp(rangeDel.SmallestKey().UserKey, m.SmallestPointKey.UserKey) != 0) && + (pointKey == nil || d.cmp(pointKey.UserKey, m.SmallestPointKey.UserKey) != 0) { + panic(errors.Newf("pebble: virtual sstable %s lower point key bound is not tight", m.FileNum)) + } + + pointKey, _ = pointIter.Last() + rangeDel = nil + if rangeDelIter != nil { + rangeDel = rangeDelIter.Last() + } + + // Check that the upper bound is tight. + if (rangeDel == nil || d.cmp(rangeDel.LargestKey().UserKey, m.LargestPointKey.UserKey) != 0) && + (pointKey == nil || d.cmp(pointKey.UserKey, m.LargestPointKey.UserKey) != 0) { + panic(errors.Newf("pebble: virtual sstable %s upper point key bound is not tight", m.FileNum)) + } + + // Check that iterator keys are within bounds. + for key, _ := pointIter.First(); key != nil; key, _ = pointIter.Next() { + if d.cmp(key.UserKey, m.SmallestPointKey.UserKey) < 0 || d.cmp(key.UserKey, m.LargestPointKey.UserKey) > 0 { + panic(errors.Newf("pebble: virtual sstable %s point key %s is not within bounds", m.FileNum, key.UserKey)) + } + } + + if rangeDelIter != nil { + for key := rangeDelIter.First(); key != nil; key = rangeDelIter.Next() { + if d.cmp(key.SmallestKey().UserKey, m.SmallestPointKey.UserKey) < 0 { + panic(errors.Newf("pebble: virtual sstable %s point key %s is not within bounds", m.FileNum, key.SmallestKey().UserKey)) + } + + if d.cmp(key.LargestKey().UserKey, m.LargestPointKey.UserKey) > 0 { + panic(errors.Newf("pebble: virtual sstable %s point key %s is not within bounds", m.FileNum, key.LargestKey().UserKey)) + } + } + } + } + + if !m.HasRangeKeys { + return + } + + rangeKeyIter, err := d.tableNewRangeKeyIter(m, keyspan.SpanIterOptions{}) + defer rangeKeyIter.Close() + + if err != nil { + panic(errors.Wrap(err, "pebble: error creating range key iterator")) + } + + // Check that the lower bound is tight. + if d.cmp(rangeKeyIter.First().SmallestKey().UserKey, m.SmallestRangeKey.UserKey) != 0 { + panic(errors.Newf("pebble: virtual sstable %s lower range key bound is not tight", m.FileNum)) + } + + // Check that upper bound is tight. + if d.cmp(rangeKeyIter.Last().LargestKey().UserKey, m.LargestRangeKey.UserKey) != 0 { + panic(errors.Newf("pebble: virtual sstable %s upper range key bound is not tight", m.FileNum)) + } + + for key := rangeKeyIter.First(); key != nil; key = rangeKeyIter.Next() { + if d.cmp(key.SmallestKey().UserKey, m.SmallestRangeKey.UserKey) < 0 { + panic(errors.Newf("pebble: virtual sstable %s point key %s is not within bounds", m.FileNum, key.SmallestKey().UserKey)) + } + if d.cmp(key.LargestKey().UserKey, m.LargestRangeKey.UserKey) > 0 { + panic(errors.Newf("pebble: virtual sstable %s point key %s is not within bounds", m.FileNum, key.LargestKey().UserKey)) + } + } +} diff --git a/vendor/github.com/cockroachdb/pebble/event.go b/vendor/github.com/cockroachdb/pebble/event.go index f243e24..d431d07 100644 --- a/vendor/github.com/cockroachdb/pebble/event.go +++ b/vendor/github.com/cockroachdb/pebble/event.go @@ -39,10 +39,11 @@ func formatFileNums(tables []TableInfo) string { return buf.String() } -// LevelInfo contains info pertaining to a partificular level. +// LevelInfo contains info pertaining to a particular level. type LevelInfo struct { Level int Tables []TableInfo + Score float64 } func (i LevelInfo) String() string { @@ -51,8 +52,11 @@ func (i LevelInfo) String() string { // SafeFormat implements redact.SafeFormatter. func (i LevelInfo) SafeFormat(w redact.SafePrinter, _ rune) { - w.Printf("L%d [%s] (%s)", redact.Safe(i.Level), redact.Safe(formatFileNums(i.Tables)), - redact.Safe(humanize.Uint64(tablesTotalSize(i.Tables)))) + w.Printf("L%d [%s] (%s) Score=%.2f", + redact.Safe(i.Level), + redact.Safe(formatFileNums(i.Tables)), + redact.Safe(humanize.Bytes.Uint64(tablesTotalSize(i.Tables))), + redact.Safe(i.Score)) } // CompactionInfo contains the info for a compaction event. @@ -75,6 +79,27 @@ type CompactionInfo struct { TotalDuration time.Duration Done bool Err error + + SingleLevelOverlappingRatio float64 + MultiLevelOverlappingRatio float64 + + // Annotations specifies additional info to appear in a compaction's event log line + Annotations compactionAnnotations +} + +type compactionAnnotations []string + +// SafeFormat implements redact.SafeFormatter. +func (ca compactionAnnotations) SafeFormat(w redact.SafePrinter, _ rune) { + if len(ca) == 0 { + return + } + for i := range ca { + if i != 0 { + w.Print(" ") + } + w.Printf("%s", redact.SafeString(ca[i])) + } } func (i CompactionInfo) String() string { @@ -90,20 +115,25 @@ func (i CompactionInfo) SafeFormat(w redact.SafePrinter, _ rune) { } if !i.Done { - w.Printf("[JOB %d] compacting(%s) ", redact.Safe(i.JobID), redact.SafeString(i.Reason)) - w.Print(levelInfos(i.Input)) + w.Printf("[JOB %d] compacting(%s) ", + redact.Safe(i.JobID), + redact.SafeString(i.Reason)) + w.Printf("%s", i.Annotations) + w.Printf("%s; ", levelInfos(i.Input)) + w.Printf("OverlappingRatio: Single %.2f, Multi %.2f", i.SingleLevelOverlappingRatio, i.MultiLevelOverlappingRatio) return } outputSize := tablesTotalSize(i.Output.Tables) w.Printf("[JOB %d] compacted(%s) ", redact.Safe(i.JobID), redact.SafeString(i.Reason)) + w.Printf("%s", i.Annotations) w.Print(levelInfos(i.Input)) w.Printf(" -> L%d [%s] (%s), in %.1fs (%.1fs total), output rate %s/s", redact.Safe(i.Output.Level), redact.Safe(formatFileNums(i.Output.Tables)), - redact.Safe(humanize.Uint64(outputSize)), + redact.Safe(humanize.Bytes.Uint64(outputSize)), redact.Safe(i.Duration.Seconds()), redact.Safe(i.TotalDuration.Seconds()), - redact.Safe(humanize.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) + redact.Safe(humanize.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) } type levelInfos []LevelInfo @@ -129,6 +159,9 @@ type FlushInfo struct { Reason string // Input contains the count of input memtables that were flushed. Input int + // InputBytes contains the total in-memory size of the memtable(s) that were + // flushed. This size includes skiplist indexing data structures. + InputBytes uint64 // Output contains the ouptut table generated by the flush. The output info // is empty for the flush begin event. Output []TableInfo @@ -168,7 +201,7 @@ func (i FlushInfo) SafeFormat(w redact.SafePrinter, _ rune) { if !i.Ingest { w.Printf("flushing %d memtable", redact.Safe(i.Input)) w.SafeString(plural) - w.Printf(" to L0") + w.Printf(" (%s) to L0", redact.Safe(humanize.Bytes.Uint64(i.InputBytes))) } else { w.Printf("flushing %d ingested table%s", redact.Safe(i.Input), plural) } @@ -180,13 +213,14 @@ func (i FlushInfo) SafeFormat(w redact.SafePrinter, _ rune) { if invariants.Enabled && len(i.IngestLevels) > 0 { panic(errors.AssertionFailedf("pebble: expected len(IngestedLevels) == 0")) } - w.Printf("[JOB %d] flushed %d memtable%s to L0 [%s] (%s), in %.1fs (%.1fs total), output rate %s/s", + w.Printf("[JOB %d] flushed %d memtable%s (%s) to L0 [%s] (%s), in %.1fs (%.1fs total), output rate %s/s", redact.Safe(i.JobID), redact.Safe(i.Input), plural, + redact.Safe(humanize.Bytes.Uint64(i.InputBytes)), redact.Safe(formatFileNums(i.Output)), - redact.Safe(humanize.Uint64(outputSize)), + redact.Safe(humanize.Bytes.Uint64(outputSize)), redact.Safe(i.Duration.Seconds()), redact.Safe(i.TotalDuration.Seconds()), - redact.Safe(humanize.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) + redact.Safe(humanize.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) } else { if invariants.Enabled && len(i.IngestLevels) == 0 { panic(errors.AssertionFailedf("pebble: expected len(IngestedLevels) > 0")) @@ -198,12 +232,12 @@ func (i FlushInfo) SafeFormat(w redact.SafePrinter, _ rune) { if j > 0 { w.Printf(" +") } - w.Printf(" L%d:%s (%s)", level, redact.Safe(file.FileNum), humanize.IEC.Uint64(file.Size)) + w.Printf(" L%d:%s (%s)", level, file.FileNum, humanize.Bytes.Uint64(file.Size)) } w.Printf(" in %.1fs (%.1fs total), output rate %s/s", redact.Safe(i.Duration.Seconds()), redact.Safe(i.TotalDuration.Seconds()), - redact.Safe(humanize.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) + redact.Safe(humanize.Bytes.Uint64(uint64(float64(outputSize)/i.Duration.Seconds())))) } } @@ -227,7 +261,7 @@ func (i ManifestCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) { w.Printf("[JOB %d] MANIFEST create error: %s", redact.Safe(i.JobID), i.Err) return } - w.Printf("[JOB %d] MANIFEST created %s", redact.Safe(i.JobID), redact.Safe(i.FileNum)) + w.Printf("[JOB %d] MANIFEST created %s", redact.Safe(i.JobID), i.FileNum) } // ManifestDeleteInfo contains the info for a Manifest deletion event. @@ -249,7 +283,7 @@ func (i ManifestDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) { w.Printf("[JOB %d] MANIFEST delete error: %s", redact.Safe(i.JobID), i.Err) return } - w.Printf("[JOB %d] MANIFEST deleted %s", redact.Safe(i.JobID), redact.Safe(i.FileNum)) + w.Printf("[JOB %d] MANIFEST deleted %s", redact.Safe(i.JobID), i.FileNum) } // TableCreateInfo contains the info for a table creation event. @@ -269,7 +303,7 @@ func (i TableCreateInfo) String() string { // SafeFormat implements redact.SafeFormatter. func (i TableCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) { w.Printf("[JOB %d] %s: sstable created %s", - redact.Safe(i.JobID), redact.Safe(i.Reason), redact.Safe(i.FileNum)) + redact.Safe(i.JobID), redact.Safe(i.Reason), i.FileNum) } // TableDeleteInfo contains the info for a table deletion event. @@ -288,10 +322,10 @@ func (i TableDeleteInfo) String() string { func (i TableDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) { if i.Err != nil { w.Printf("[JOB %d] sstable delete error %s: %s", - redact.Safe(i.JobID), redact.Safe(i.FileNum), i.Err) + redact.Safe(i.JobID), i.FileNum, i.Err) return } - w.Printf("[JOB %d] sstable deleted %s", redact.Safe(i.JobID), redact.Safe(i.FileNum)) + w.Printf("[JOB %d] sstable deleted %s", redact.Safe(i.JobID), i.FileNum) } // TableIngestInfo contains the info for a table ingestion event. @@ -337,8 +371,8 @@ func (i TableIngestInfo) SafeFormat(w redact.SafePrinter, _ rune) { if !i.flushable { levelStr = fmt.Sprintf("L%d:", t.Level) } - w.Printf(" %s%s (%s)", redact.Safe(levelStr), redact.Safe(t.FileNum), - redact.Safe(humanize.Uint64(t.Size))) + w.Printf(" %s%s (%s)", redact.Safe(levelStr), t.FileNum, + redact.Safe(humanize.Bytes.Uint64(t.Size))) } } @@ -399,12 +433,12 @@ func (i WALCreateInfo) SafeFormat(w redact.SafePrinter, _ rune) { } if i.RecycledFileNum == 0 { - w.Printf("[JOB %d] WAL created %s", redact.Safe(i.JobID), redact.Safe(i.FileNum)) + w.Printf("[JOB %d] WAL created %s", redact.Safe(i.JobID), i.FileNum) return } w.Printf("[JOB %d] WAL created %s (recycled %s)", - redact.Safe(i.JobID), redact.Safe(i.FileNum), redact.Safe(i.RecycledFileNum)) + redact.Safe(i.JobID), i.FileNum, i.RecycledFileNum) } // WALDeleteInfo contains the info for a WAL deletion event. @@ -426,7 +460,7 @@ func (i WALDeleteInfo) SafeFormat(w redact.SafePrinter, _ rune) { w.Printf("[JOB %d] WAL delete error: %s", redact.Safe(i.JobID), i.Err) return } - w.Printf("[JOB %d] WAL deleted %s", redact.Safe(i.JobID), redact.Safe(i.FileNum)) + w.Printf("[JOB %d] WAL deleted %s", redact.Safe(i.JobID), i.FileNum) } // WriteStallBeginInfo contains the info for a write stall begin event. diff --git a/vendor/github.com/cockroachdb/pebble/external_iterator.go b/vendor/github.com/cockroachdb/pebble/external_iterator.go index 3c45c2c..1bd00c3 100644 --- a/vendor/github.com/cockroachdb/pebble/external_iterator.go +++ b/vendor/github.com/cockroachdb/pebble/external_iterator.go @@ -209,15 +209,15 @@ func createExternalPointIter(ctx context.Context, it *Iterator) (internalIterato pointIter internalIterator err error ) - pointIter, err = r.NewIterWithBlockPropertyFiltersAndContext( - ctx, - it.opts.LowerBound, - it.opts.UpperBound, - nil, /* BlockPropertiesFilterer */ - false, /* useFilterBlock */ - &it.stats.InternalStats, - sstable.TrivialReaderProvider{Reader: r}, - ) + // We could set hideObsoletePoints=true, since we are reading at + // InternalKeySeqNumMax, but we don't bother since these sstables should + // not have obsolete points (so the performance optimization is + // unnecessary), and we don't want to bother constructing a + // BlockPropertiesFilterer that includes obsoleteKeyBlockPropertyFilter. + pointIter, err = r.NewIterWithBlockPropertyFiltersAndContextEtc( + ctx, it.opts.LowerBound, it.opts.UpperBound, nil, /* BlockPropertiesFilterer */ + false /* hideObsoletePoints */, false, /* useFilterBlock */ + &it.stats.InternalStats, sstable.TrivialReaderProvider{Reader: r}) if err != nil { return nil, err } @@ -311,7 +311,7 @@ func finishInitializingExternal(ctx context.Context, it *Iterator) { base.InternalKeySeqNumMax, it.opts.LowerBound, it.opts.UpperBound, &it.hasPrefix, &it.prefixOrFullSeekKey, - true /* onlySets */, &it.rangeKey.internal, + false /* internalKeys */, &it.rangeKey.internal, ) for i := range rangeKeyIters { it.rangeKey.iterConfig.AddLevel(rangeKeyIters[i]) @@ -319,8 +319,12 @@ func finishInitializingExternal(ctx context.Context, it *Iterator) { } } if it.rangeKey != nil { - it.rangeKey.iiter.Init(&it.comparer, it.iter, it.rangeKey.rangeKeyIter, &it.rangeKeyMasking, - it.opts.LowerBound, it.opts.UpperBound) + it.rangeKey.iiter.Init(&it.comparer, it.iter, it.rangeKey.rangeKeyIter, + keyspan.InterleavingIterOpts{ + Mask: &it.rangeKeyMasking, + LowerBound: it.opts.LowerBound, + UpperBound: it.opts.UpperBound, + }) it.iter = &it.rangeKey.iiter } } diff --git a/vendor/github.com/cockroachdb/pebble/filenames.go b/vendor/github.com/cockroachdb/pebble/filenames.go index bc33b38..07d74c8 100644 --- a/vendor/github.com/cockroachdb/pebble/filenames.go +++ b/vendor/github.com/cockroachdb/pebble/filenames.go @@ -33,7 +33,7 @@ const ( // NB: This is a low-level routine and typically not what you want to // use. Newer versions of Pebble running newer format major versions do // not use the CURRENT file. See setCurrentFunc in version_set.go. -func setCurrentFile(dirname string, fs vfs.FS, fileNum FileNum) error { +func setCurrentFile(dirname string, fs vfs.FS, fileNum base.DiskFileNum) error { newFilename := base.MakeFilepath(fs, dirname, fileTypeCurrent, fileNum) oldFilename := base.MakeFilepath(fs, dirname, fileTypeTemp, fileNum) fs.Remove(oldFilename) diff --git a/vendor/github.com/cockroachdb/pebble/flushable.go b/vendor/github.com/cockroachdb/pebble/flushable.go index 13fca5f..09abee3 100644 --- a/vendor/github.com/cockroachdb/pebble/flushable.go +++ b/vendor/github.com/cockroachdb/pebble/flushable.go @@ -60,20 +60,20 @@ type flushableEntry struct { // flushable is a memTable, when the reader refs drops to zero, the writer // refs will already be zero because the memtable will have been flushed and // that only occurs once the writer refs drops to zero. - readerRefs int32 + readerRefs atomic.Int32 // Closure to invoke to release memory accounting. releaseMemAccounting func() // unrefFiles, if not nil, should be invoked to decrease the ref count of // files which are backing the flushable. - unrefFiles func() []*fileMetadata + unrefFiles func() []*fileBacking // deleteFnLocked should be called if the caller is holding DB.mu. - deleteFnLocked func(obsolete []*fileMetadata) + deleteFnLocked func(obsolete []*fileBacking) // deleteFn should be called if the caller is not holding DB.mu. - deleteFn func(obsolete []*fileMetadata) + deleteFn func(obsolete []*fileBacking) } func (e *flushableEntry) readerRef() { - switch v := atomic.AddInt32(&e.readerRefs, 1); { + switch v := e.readerRefs.Add(1); { case v <= 1: panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) } @@ -90,9 +90,9 @@ func (e *flushableEntry) readerUnrefLocked(deleteFiles bool) { } func (e *flushableEntry) readerUnrefHelper( - deleteFiles bool, deleteFn func(obsolete []*manifest.FileMetadata), + deleteFiles bool, deleteFn func(obsolete []*fileBacking), ) { - switch v := atomic.AddInt32(&e.readerRefs, -1); { + switch v := e.readerRefs.Add(-1); { case v < 0: panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) case v == 0: @@ -116,9 +116,8 @@ type flushableList []*flushableEntry // ingestedFlushable is the implementation of the flushable interface for the // ingesting sstables which are added to the flushable list. type ingestedFlushable struct { - files []*fileMetadata - cmp Compare - split Split + files []physicalMeta + comparer *Comparer newIters tableNewIters newRangeKeyIters keyspan.TableNewSpanIter @@ -131,26 +130,27 @@ type ingestedFlushable struct { func newIngestedFlushable( files []*fileMetadata, - cmp Compare, - split Split, + comparer *Comparer, newIters tableNewIters, newRangeKeyIters keyspan.TableNewSpanIter, ) *ingestedFlushable { + var physicalFiles []physicalMeta + var hasRangeKeys bool + for _, f := range files { + if f.HasRangeKeys { + hasRangeKeys = true + } + physicalFiles = append(physicalFiles, f.PhysicalMeta()) + } + ret := &ingestedFlushable{ - files: files, - cmp: cmp, - split: split, + files: physicalFiles, + comparer: comparer, newIters: newIters, newRangeKeyIters: newRangeKeyIters, // slice is immutable and can be set once and used many times. - slice: manifest.NewLevelSliceKeySorted(cmp, files), - } - - for _, f := range files { - if f.HasRangeKeys { - ret.hasRangeKeys = true - break - } + slice: manifest.NewLevelSliceKeySorted(comparer.Compare, files), + hasRangeKeys: hasRangeKeys, } return ret @@ -159,6 +159,7 @@ func newIngestedFlushable( // TODO(sumeer): ingestedFlushable iters also need to plumb context for // tracing. +// newIter is part of the flushable interface. func (s *ingestedFlushable) newIter(o *IterOptions) internalIterator { var opts IterOptions if o != nil { @@ -169,10 +170,11 @@ func (s *ingestedFlushable) newIter(o *IterOptions) internalIterator { // aren't truly levels in the lsm. Right now, the encoding only supports // L0 sublevels, and the rest of the levels in the lsm. return newLevelIter( - opts, s.cmp, s.split, s.newIters, s.slice.Iter(), manifest.Level(0), nil, + opts, s.comparer, s.newIters, s.slice.Iter(), manifest.Level(0), internalIterOpts{}, ) } +// newFlushIter is part of the flushable interface. func (s *ingestedFlushable) newFlushIter(o *IterOptions, bytesFlushed *uint64) internalIterator { // newFlushIter is only used for writing memtables to disk as sstables. // Since ingested sstables are already present on disk, they don't need to @@ -181,7 +183,7 @@ func (s *ingestedFlushable) newFlushIter(o *IterOptions, bytesFlushed *uint64) i } func (s *ingestedFlushable) constructRangeDelIter( - file *manifest.FileMetadata, _ *keyspan.SpanIterOptions, + file *manifest.FileMetadata, _ keyspan.SpanIterOptions, ) (keyspan.FragmentIterator, error) { // Note that the keyspan level iter expects a non-nil iterator to be // returned even if there is an error. So, we return the emptyKeyspanIter. @@ -196,41 +198,47 @@ func (s *ingestedFlushable) constructRangeDelIter( return rangeDelIter, nil } +// newRangeDelIter is part of the flushable interface. // TODO(bananabrick): Using a level iter instead of a keyspan level iter to // surface range deletes is more efficient. func (s *ingestedFlushable) newRangeDelIter(_ *IterOptions) keyspan.FragmentIterator { return keyspan.NewLevelIter( - keyspan.SpanIterOptions{}, s.cmp, + keyspan.SpanIterOptions{}, s.comparer.Compare, s.constructRangeDelIter, s.slice.Iter(), manifest.Level(0), manifest.KeyTypePoint, ) } +// newRangeKeyIter is part of the flushable interface. func (s *ingestedFlushable) newRangeKeyIter(o *IterOptions) keyspan.FragmentIterator { if !s.containsRangeKeys() { return nil } return keyspan.NewLevelIter( - keyspan.SpanIterOptions{}, s.cmp, s.newRangeKeyIters, + keyspan.SpanIterOptions{}, s.comparer.Compare, s.newRangeKeyIters, s.slice.Iter(), manifest.Level(0), manifest.KeyTypeRange, ) } +// containsRangeKeys is part of the flushable interface. func (s *ingestedFlushable) containsRangeKeys() bool { return s.hasRangeKeys } +// inuseBytes is part of the flushable interface. func (s *ingestedFlushable) inuseBytes() uint64 { // inuseBytes is only used when memtables are flushed to disk as sstables. panic("pebble: not implemented") } +// totalBytes is part of the flushable interface. func (s *ingestedFlushable) totalBytes() uint64 { // We don't allocate additional bytes for the ingestedFlushable. return 0 } +// readyForFlush is part of the flushable interface. func (s *ingestedFlushable) readyForFlush() bool { // ingestedFlushable should always be ready to flush. However, note that // memtables before the ingested sstables in the memtable queue must be diff --git a/vendor/github.com/cockroachdb/pebble/format_major_version.go b/vendor/github.com/cockroachdb/pebble/format_major_version.go index 9e96709..45f1bc9 100644 --- a/vendor/github.com/cockroachdb/pebble/format_major_version.go +++ b/vendor/github.com/cockroachdb/pebble/format_major_version.go @@ -28,10 +28,13 @@ import ( // FormatVersion that the default corresponds to may change with time. type FormatMajorVersion uint64 +// SafeValue implements redact.SafeValue. +func (v FormatMajorVersion) SafeValue() {} + // String implements fmt.Stringer. func (v FormatMajorVersion) String() string { - // NB: This must not change. It's used as the value for the the - // on-disk version marker file. + // NB: This must not change. It's used as the value for the on-disk + // version marker file. // // Specifically, this value must always parse as a base 10 integer // that fits in a uint64. We format it as zero-padded, 3-digit @@ -110,12 +113,12 @@ const ( // 23.1 versions. - // FormatUnusedPrePebblev1MarkedCompacted is an unused format major version. + // formatUnusedPrePebblev1MarkedCompacted is an unused format major version. // This format major version was originally intended to ship in the 23.1 // release. It was later decided that this should be deferred until a // subsequent release. The original ordering is preserved so as not to // introduce breaking changes in Cockroach. - FormatUnusedPrePebblev1MarkedCompacted + formatUnusedPrePebblev1MarkedCompacted // FormatSSTableValueBlocks is a format major version that adds support for // storing values in value blocks in the sstable. Value block support is not @@ -151,8 +154,28 @@ const ( // compactions for files marked for compaction are complete. FormatPrePebblev1MarkedCompacted + // FormatDeleteSizedAndObsolete is a format major version that adds support + // for deletion tombstones that encode the size of the value they're + // expected to delete. This format major version is required before the + // associated key kind may be committed through batch applications or + // ingests. It also adds support for keys that are marked obsolete (see + // sstable/format.go for details). + FormatDeleteSizedAndObsolete + + // FormatVirtualSSTables is a format major version that adds support for + // virtual sstables that can reference a sub-range of keys in an underlying + // physical sstable. This information is persisted through new, + // backward-incompatible fields in the Manifest, and therefore requires + // a format major version. + FormatVirtualSSTables + + // internalFormatNewest holds the newest format major version, including + // experimental ones excluded from the exported FormatNewest constant until + // they've stabilized. Used in tests. + internalFormatNewest FormatMajorVersion = iota - 1 + // FormatNewest always contains the most recent format major version. - FormatNewest FormatMajorVersion = iota - 1 + FormatNewest FormatMajorVersion = internalFormatNewest ) // MaxTableFormat returns the maximum sstable.TableFormat that can be used at @@ -166,11 +189,12 @@ func (v FormatMajorVersion) MaxTableFormat() sstable.TableFormat { FormatSplitUserKeysMarkedCompacted: return sstable.TableFormatPebblev1 case FormatRangeKeys, FormatMinTableFormatPebblev1, FormatPrePebblev1Marked, - FormatUnusedPrePebblev1MarkedCompacted: + formatUnusedPrePebblev1MarkedCompacted: return sstable.TableFormatPebblev2 - case FormatSSTableValueBlocks, FormatFlushableIngest, - FormatPrePebblev1MarkedCompacted: + case FormatSSTableValueBlocks, FormatFlushableIngest, FormatPrePebblev1MarkedCompacted: return sstable.TableFormatPebblev3 + case FormatDeleteSizedAndObsolete, FormatVirtualSSTables: + return sstable.TableFormatPebblev4 default: panic(fmt.Sprintf("pebble: unsupported format major version: %s", v)) } @@ -186,14 +210,27 @@ func (v FormatMajorVersion) MinTableFormat() sstable.TableFormat { FormatRangeKeys: return sstable.TableFormatLevelDB case FormatMinTableFormatPebblev1, FormatPrePebblev1Marked, - FormatUnusedPrePebblev1MarkedCompacted, FormatSSTableValueBlocks, - FormatFlushableIngest, FormatPrePebblev1MarkedCompacted: + formatUnusedPrePebblev1MarkedCompacted, FormatSSTableValueBlocks, + FormatFlushableIngest, FormatPrePebblev1MarkedCompacted, + FormatDeleteSizedAndObsolete, FormatVirtualSSTables: return sstable.TableFormatPebblev1 default: panic(fmt.Sprintf("pebble: unsupported format major version: %s", v)) } } +// orderingInvariants returns an enum encoding the set of invariants that must +// hold within the receiver format major version. Invariants only get stricter +// as the format major version advances, so it is okay to retrieve the +// invariants from the current format major version and by the time the +// invariants are enforced, the format major version has advanced. +func (v FormatMajorVersion) orderingInvariants() manifest.OrderingInvariants { + if v < FormatSplitUserKeysMarkedCompacted { + return manifest.AllowSplitUserKeys + } + return manifest.ProhibitSplitUserKeys +} + // formatMajorVersionMigrations defines the migrations from one format // major version to the next. Each migration is defined as a closure // which will be invoked on the database before the new format major @@ -219,7 +256,7 @@ var formatMajorVersionMigrations = map[FormatMajorVersion]func(*DB) error{ // guaranteed to exist, because we unconditionally locate it // during Open. manifestFileNum := d.mu.versions.manifestFileNum - filename := base.MakeFilename(fileTypeManifest, manifestFileNum) + filename := base.MakeFilename(fileTypeManifest, manifestFileNum.DiskFileNum()) if err := d.mu.versions.manifestMarker.Move(filename); err != nil { return errors.Wrap(err, "moving manifest marker") } @@ -257,7 +294,7 @@ var formatMajorVersionMigrations = map[FormatMajorVersion]func(*DB) error{ // version that does not know about format major versions // attempts to open the database, it will error avoiding // accidental corruption. - if err := setCurrentFile(d.mu.versions.dirname, d.mu.versions.fs, 0); err != nil { + if err := setCurrentFile(d.mu.versions.dirname, d.mu.versions.fs, base.FileNum(0).DiskFileNum()); err != nil { return err } return d.finalizeFormatVersUpgrade(FormatVersioned) @@ -301,9 +338,9 @@ var formatMajorVersionMigrations = map[FormatMajorVersion]func(*DB) error{ } return d.finalizeFormatVersUpgrade(FormatPrePebblev1Marked) }, - FormatUnusedPrePebblev1MarkedCompacted: func(d *DB) error { + formatUnusedPrePebblev1MarkedCompacted: func(d *DB) error { // Intentional no-op. - return d.finalizeFormatVersUpgrade(FormatUnusedPrePebblev1MarkedCompacted) + return d.finalizeFormatVersUpgrade(formatUnusedPrePebblev1MarkedCompacted) }, FormatSSTableValueBlocks: func(d *DB) error { return d.finalizeFormatVersUpgrade(FormatSSTableValueBlocks) @@ -320,6 +357,12 @@ var formatMajorVersionMigrations = map[FormatMajorVersion]func(*DB) error{ } return d.finalizeFormatVersUpgrade(FormatPrePebblev1MarkedCompacted) }, + FormatDeleteSizedAndObsolete: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatDeleteSizedAndObsolete) + }, + FormatVirtualSSTables: func(d *DB) error { + return d.finalizeFormatVersUpgrade(FormatVirtualSSTables) + }, } const formatVersionMarkerName = `format-version` @@ -342,7 +385,7 @@ func lookupFormatMajorVersion( if vers == FormatDefault { return 0, nil, errors.Newf("pebble: default format major version should not persisted", vers) } - if vers > FormatNewest { + if vers > internalFormatNewest { return 0, nil, errors.Newf("pebble: database %q written in format major version %d", dirname, vers) } return vers, m, nil @@ -376,8 +419,8 @@ func (d *DB) ratchetFormatMajorVersionLocked(formatVers FormatMajorVersion) erro if d.opts.ReadOnly { return ErrReadOnly } - if formatVers > FormatNewest { - // Guard against accidentally forgetting to update FormatNewest. + if formatVers > internalFormatNewest { + // Guard against accidentally forgetting to update internalFormatNewest. return errors.Errorf("pebble: unknown format version %d", formatVers) } if currentVers := d.FormatMajorVersion(); currentVers > formatVers { @@ -519,17 +562,24 @@ var markFilesPrePebblev1 = func(tc *tableCacheContainer) findFilesFunc { for l := numLevels - 1; l > 0; l-- { iter := v.Levels[l].Iter() for f := iter.First(); f != nil; f = iter.Next() { - err = tc.withReader(f, func(r *sstable.Reader) error { - tf, err := r.TableFormat() - if err != nil { - return err - } - if tf < sstable.TableFormatPebblev1 { - found = true - files[l] = append(files[l], f) - } - return nil - }) + if f.Virtual { + // Any physical sstable which has been virtualized must + // have already undergone this migration, and we don't + // need to worry about the virtual sstable themselves. + panic("pebble: unexpected virtual sstable during migration") + } + err = tc.withReader( + f.PhysicalMeta(), func(r *sstable.Reader) error { + tf, err := r.TableFormat() + if err != nil { + return err + } + if tf < sstable.TableFormatPebblev1 { + found = true + files[l] = append(files[l], f) + } + return nil + }) if err != nil { return } diff --git a/vendor/github.com/cockroachdb/pebble/get_iter.go b/vendor/github.com/cockroachdb/pebble/get_iter.go index f1fc25c..99c5d7c 100644 --- a/vendor/github.com/cockroachdb/pebble/get_iter.go +++ b/vendor/github.com/cockroachdb/pebble/get_iter.go @@ -19,8 +19,7 @@ import ( // lazily. type getIter struct { logger Logger - cmp Compare - equal Equal + comparer *Comparer newIters tableNewIters snapshot uint64 key []byte @@ -83,7 +82,7 @@ func (g *getIter) Next() (*InternalKey, base.LazyValue) { // key. Every call to levelIter.Next() potentially switches to a new // table and thus reinitializes rangeDelIter. if g.rangeDelIter != nil { - g.tombstone = keyspan.Get(g.cmp, g.rangeDelIter, g.key) + g.tombstone = keyspan.Get(g.comparer.Compare, g.rangeDelIter, g.key) if g.err = g.rangeDelIter.Close(); g.err != nil { return nil, base.LazyValue{} } @@ -101,7 +100,7 @@ func (g *getIter) Next() (*InternalKey, base.LazyValue) { g.iter = nil return nil, base.LazyValue{} } - if g.equal(g.key, key.UserKey) { + if g.comparer.Equal(g.key, key.UserKey) { if !key.Visible(g.snapshot, base.InternalKeySeqNumMax) { g.iterKey, g.iterValue = g.iter.Next() continue @@ -158,12 +157,25 @@ func (g *getIter) Next() (*InternalKey, base.LazyValue) { if n := len(g.l0); n > 0 { files := g.l0[n-1].Iter() g.l0 = g.l0[:n-1] - iterOpts := IterOptions{logger: g.logger} - g.levelIter.init(context.Background(), iterOpts, g.cmp, nil /* split */, g.newIters, + iterOpts := IterOptions{logger: g.logger, snapshotForHideObsoletePoints: g.snapshot} + g.levelIter.init(context.Background(), iterOpts, g.comparer, g.newIters, files, manifest.L0Sublevel(n), internalIterOpts{}) g.levelIter.initRangeDel(&g.rangeDelIter) + bc := levelIterBoundaryContext{} + g.levelIter.initBoundaryContext(&bc) g.iter = &g.levelIter - g.iterKey, g.iterValue = g.iter.SeekGE(g.key, base.SeekGEFlagsNone) + + // Compute the key prefix for bloom filtering if split function is + // specified, or use the user key as default. + prefix := g.key + if g.comparer.Split != nil { + prefix = g.key[:g.comparer.Split(g.key)] + } + g.iterKey, g.iterValue = g.iter.SeekPrefixGE(prefix, g.key, base.SeekGEFlagsNone) + if bc.isSyntheticIterBoundsKey || bc.isIgnorableBoundaryKey { + g.iterKey = nil + g.iterValue = base.LazyValue{} + } continue } g.level++ @@ -177,13 +189,26 @@ func (g *getIter) Next() (*InternalKey, base.LazyValue) { continue } - iterOpts := IterOptions{logger: g.logger} - g.levelIter.init(context.Background(), iterOpts, g.cmp, nil /* split */, g.newIters, + iterOpts := IterOptions{logger: g.logger, snapshotForHideObsoletePoints: g.snapshot} + g.levelIter.init(context.Background(), iterOpts, g.comparer, g.newIters, g.version.Levels[g.level].Iter(), manifest.Level(g.level), internalIterOpts{}) g.levelIter.initRangeDel(&g.rangeDelIter) + bc := levelIterBoundaryContext{} + g.levelIter.initBoundaryContext(&bc) g.level++ g.iter = &g.levelIter - g.iterKey, g.iterValue = g.iter.SeekGE(g.key, base.SeekGEFlagsNone) + + // Compute the key prefix for bloom filtering if split function is + // specified, or use the user key as default. + prefix := g.key + if g.comparer.Split != nil { + prefix = g.key[:g.comparer.Split(g.key)] + } + g.iterKey, g.iterValue = g.iter.SeekPrefixGE(prefix, g.key, base.SeekGEFlagsNone) + if bc.isSyntheticIterBoundsKey || bc.isIgnorableBoundaryKey { + g.iterKey = nil + g.iterValue = base.LazyValue{} + } } } diff --git a/vendor/github.com/cockroachdb/pebble/ingest.go b/vendor/github.com/cockroachdb/pebble/ingest.go index bd4b34b..fa6304a 100644 --- a/vendor/github.com/cockroachdb/pebble/ingest.go +++ b/vendor/github.com/cockroachdb/pebble/ingest.go @@ -7,15 +7,16 @@ package pebble import ( "context" "sort" - "sync/atomic" "time" "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" "github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/private" "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/remote" "github.com/cockroachdb/pebble/sstable" ) @@ -34,6 +35,47 @@ func sstableKeyCompare(userCmp Compare, a, b InternalKey) int { return 0 } +// KeyRange encodes a key range in user key space. A KeyRange's Start is +// inclusive while its End is exclusive. +type KeyRange struct { + Start, End []byte +} + +// Valid returns true if the KeyRange is defined. +func (k *KeyRange) Valid() bool { + return k.Start != nil && k.End != nil +} + +// Contains returns whether the specified key exists in the KeyRange. +func (k *KeyRange) Contains(cmp base.Compare, key InternalKey) bool { + v := cmp(key.UserKey, k.End) + return (v < 0 || (v == 0 && key.IsExclusiveSentinel())) && cmp(k.Start, key.UserKey) <= 0 +} + +// OverlapsInternalKeyRange checks if the specified internal key range has an +// overlap with the KeyRange. Note that we aren't checking for full containment +// of smallest-largest within k, rather just that there's some intersection +// between the two ranges. +func (k *KeyRange) OverlapsInternalKeyRange(cmp base.Compare, smallest, largest InternalKey) bool { + v := cmp(k.Start, largest.UserKey) + return v <= 0 && !(largest.IsExclusiveSentinel() && v == 0) && + cmp(k.End, smallest.UserKey) > 0 +} + +// Overlaps checks if the specified file has an overlap with the KeyRange. +// Note that we aren't checking for full containment of m within k, rather just +// that there's some intersection between m and k's bounds. +func (k *KeyRange) Overlaps(cmp base.Compare, m *fileMetadata) bool { + return k.OverlapsInternalKeyRange(cmp, m.Smallest, m.Largest) +} + +// OverlapsKeyRange checks if this span overlaps with the provided KeyRange. +// Note that we aren't checking for full containment of either span in the other, +// just that there's a key x that is in both key ranges. +func (k *KeyRange) OverlapsKeyRange(cmp Compare, span KeyRange) bool { + return cmp(k.Start, span.End) < 0 && cmp(k.End, span.Start) > 0 +} + func ingestValidateKey(opts *Options, key *InternalKey) error { if key.Kind() == InternalKeyKindInvalid { return base.CorruptionErrorf("pebble: external sstable has corrupted key: %s", @@ -46,19 +88,143 @@ func ingestValidateKey(opts *Options, key *InternalKey) error { return nil } -func ingestLoad1( - opts *Options, fmv FormatMajorVersion, path string, cacheID uint64, fileNum FileNum, +// ingestSynthesizeShared constructs a fileMetadata for one shared sstable owned +// or shared by another node. +func ingestSynthesizeShared( + opts *Options, sm SharedSSTMeta, fileNum base.DiskFileNum, ) (*fileMetadata, error) { - f, err := opts.FS.Open(path) + if sm.Size == 0 { + // Disallow 0 file sizes + return nil, errors.New("pebble: cannot ingest shared file with size 0") + } + // Don't load table stats. Doing a round trip to shared storage, one SST + // at a time is not worth it as it slows down ingestion. + meta := &fileMetadata{ + FileNum: fileNum.FileNum(), + CreationTime: time.Now().Unix(), + Virtual: true, + Size: sm.Size, + } + meta.InitProviderBacking(fileNum) + // Set the underlying FileBacking's size to the same size as the virtualized + // view of the sstable. This ensures that we don't over-prioritize this + // sstable for compaction just yet, as we do not have a clear sense of what + // parts of this sstable are referenced by other nodes. + meta.FileBacking.Size = sm.Size + if sm.LargestRangeKey.Valid() && sm.LargestRangeKey.UserKey != nil { + // Initialize meta.{HasRangeKeys,Smallest,Largest}, etc. + // + // NB: We create new internal keys and pass them into ExternalRangeKeyBounds + // so that we can sub a zero sequence number into the bounds. We can set + // the sequence number to anything here; it'll be reset in ingestUpdateSeqNum + // anyway. However we do need to use the same sequence number across all + // bound keys at this step so that we end up with bounds that are consistent + // across point/range keys. + smallestRangeKey := base.MakeInternalKey(sm.SmallestRangeKey.UserKey, 0, sm.SmallestRangeKey.Kind()) + largestRangeKey := base.MakeExclusiveSentinelKey(sm.LargestRangeKey.Kind(), sm.LargestRangeKey.UserKey) + meta.ExtendRangeKeyBounds(opts.Comparer.Compare, smallestRangeKey, largestRangeKey) + } + if sm.LargestPointKey.Valid() && sm.LargestPointKey.UserKey != nil { + // Initialize meta.{HasPointKeys,Smallest,Largest}, etc. + // + // See point above in the ExtendRangeKeyBounds call on why we use a zero + // sequence number here. + smallestPointKey := base.MakeInternalKey(sm.SmallestPointKey.UserKey, 0, sm.SmallestPointKey.Kind()) + largestPointKey := base.MakeInternalKey(sm.LargestPointKey.UserKey, 0, sm.LargestPointKey.Kind()) + if sm.LargestPointKey.IsExclusiveSentinel() { + largestPointKey = base.MakeRangeDeleteSentinelKey(sm.LargestPointKey.UserKey) + } + meta.ExtendPointKeyBounds(opts.Comparer.Compare, smallestPointKey, largestPointKey) + } + if err := meta.Validate(opts.Comparer.Compare, opts.Comparer.FormatKey); err != nil { + return nil, err + } + return meta, nil +} + +// ingestLoad1External loads the fileMetadata for one external sstable. +// Sequence number and target level calculation happens during prepare/apply. +func ingestLoad1External( + opts *Options, + e ExternalFile, + fileNum base.DiskFileNum, + objprovider objstorage.Provider, + jobID int, +) (*fileMetadata, error) { + if e.Size == 0 { + // Disallow 0 file sizes + return nil, errors.New("pebble: cannot ingest external file with size 0") + } + if !e.HasRangeKey && !e.HasPointKey { + return nil, errors.New("pebble: cannot ingest external file with no point or range keys") + } + // Don't load table stats. Doing a round trip to shared storage, one SST + // at a time is not worth it as it slows down ingestion. + meta := &fileMetadata{} + meta.FileNum = fileNum.FileNum() + meta.CreationTime = time.Now().Unix() + meta.Virtual = true + meta.Size = e.Size + meta.InitProviderBacking(fileNum) + + // Try to resolve a reference to the external file. + backing, err := objprovider.CreateExternalObjectBacking(e.Locator, e.ObjName) if err != nil { return nil, err } - - readable, err := sstable.NewSimpleReadable(f) + metas, err := objprovider.AttachRemoteObjects([]objstorage.RemoteObjectToAttach{{ + FileNum: fileNum, + FileType: fileTypeTable, + Backing: backing, + }}) if err != nil { return nil, err } + if opts.EventListener.TableCreated != nil { + opts.EventListener.TableCreated(TableCreateInfo{ + JobID: jobID, + Reason: "ingesting", + Path: objprovider.Path(metas[0]), + FileNum: fileNum.FileNum(), + }) + } + // In the name of keeping this ingestion as fast as possible, we avoid + // *all* existence checks and synthesize a file metadata with smallest/largest + // keys that overlap whatever the passed-in span was. + smallestCopy := make([]byte, len(e.SmallestUserKey)) + copy(smallestCopy, e.SmallestUserKey) + largestCopy := make([]byte, len(e.LargestUserKey)) + copy(largestCopy, e.LargestUserKey) + if e.HasPointKey { + meta.ExtendPointKeyBounds(opts.Comparer.Compare, base.MakeInternalKey(smallestCopy, 0, InternalKeyKindMax), + base.MakeRangeDeleteSentinelKey(largestCopy)) + } + if e.HasRangeKey { + meta.ExtendRangeKeyBounds(opts.Comparer.Compare, base.MakeInternalKey(smallestCopy, 0, InternalKeyKindRangeKeySet), + base.MakeExclusiveSentinelKey(InternalKeyKindRangeKeyDelete, largestCopy)) + } + // Set the underlying FileBacking's size to the same size as the virtualized + // view of the sstable. This ensures that we don't over-prioritize this + // sstable for compaction just yet, as we do not have a clear sense of + // what parts of this sstable are referenced by other nodes. + meta.FileBacking.Size = e.Size + + if err := meta.Validate(opts.Comparer.Compare, opts.Comparer.FormatKey); err != nil { + return nil, err + } + return meta, nil +} + +// ingestLoad1 creates the FileMetadata for one file. This file will be owned +// by this store. +func ingestLoad1( + opts *Options, + fmv FormatMajorVersion, + readable objstorage.Readable, + cacheID uint64, + fileNum base.DiskFileNum, +) (*fileMetadata, error) { cacheOpts := private.SSTableCacheOpts(cacheID, fileNum).(sstable.ReaderOption) r, err := sstable.NewReader(readable, opts.MakeReaderOptions(), cacheOpts) if err != nil { @@ -79,9 +245,10 @@ func ingestLoad1( } meta := &fileMetadata{} - meta.FileNum = fileNum + meta.FileNum = fileNum.FileNum() meta.Size = uint64(readable.Size()) meta.CreationTime = time.Now().Unix() + meta.InitPhysicalBacking() // Avoid loading into the table cache for collecting stats if we // don't need to. If there are no range deletions, we have all the @@ -92,7 +259,7 @@ func ingestLoad1( // disallowing removal of an open file. Under MemFS, if we don't populate // meta.Stats here, the file will be loaded into the table cache for // calculating stats before we can remove the original link. - maybeSetStatsFromProperties(meta, &r.Properties) + maybeSetStatsFromProperties(meta.PhysicalMeta(), &r.Properties) { iter, err := r.NewIter(nil /* lower */, nil /* upper */) @@ -195,22 +362,83 @@ func ingestLoad1( return meta, nil } +type ingestLoadResult struct { + localMeta, sharedMeta []*fileMetadata + externalMeta []*fileMetadata + localPaths []string + sharedLevels []uint8 + fileCount int +} + func ingestLoad( - opts *Options, fmv FormatMajorVersion, paths []string, cacheID uint64, pending []FileNum, -) ([]*fileMetadata, []string, error) { + opts *Options, + fmv FormatMajorVersion, + paths []string, + shared []SharedSSTMeta, + external []ExternalFile, + cacheID uint64, + pending []base.DiskFileNum, + objProvider objstorage.Provider, + jobID int, +) (ingestLoadResult, error) { meta := make([]*fileMetadata, 0, len(paths)) newPaths := make([]string, 0, len(paths)) for i := range paths { - m, err := ingestLoad1(opts, fmv, paths[i], cacheID, pending[i]) + f, err := opts.FS.Open(paths[i]) if err != nil { - return nil, nil, err + return ingestLoadResult{}, err + } + + readable, err := sstable.NewSimpleReadable(f) + if err != nil { + return ingestLoadResult{}, err + } + m, err := ingestLoad1(opts, fmv, readable, cacheID, pending[i]) + if err != nil { + return ingestLoadResult{}, err } if m != nil { meta = append(meta, m) newPaths = append(newPaths, paths[i]) } } - return meta, newPaths, nil + if len(shared) == 0 && len(external) == 0 { + return ingestLoadResult{localMeta: meta, localPaths: newPaths, fileCount: len(meta)}, nil + } + + // Sort the shared files according to level. + sort.Sort(sharedByLevel(shared)) + + sharedMeta := make([]*fileMetadata, 0, len(shared)) + levels := make([]uint8, 0, len(shared)) + for i := range shared { + m, err := ingestSynthesizeShared(opts, shared[i], pending[len(paths)+i]) + if err != nil { + return ingestLoadResult{}, err + } + if shared[i].Level < sharedLevelsStart { + return ingestLoadResult{}, errors.New("cannot ingest shared file in level below sharedLevelsStart") + } + sharedMeta = append(sharedMeta, m) + levels = append(levels, shared[i].Level) + } + externalMeta := make([]*fileMetadata, 0, len(external)) + for i := range external { + m, err := ingestLoad1External(opts, external[i], pending[len(paths)+len(shared)+i], objProvider, jobID) + if err != nil { + return ingestLoadResult{}, err + } + externalMeta = append(externalMeta, m) + } + result := ingestLoadResult{ + localMeta: meta, + sharedMeta: sharedMeta, + externalMeta: externalMeta, + localPaths: newPaths, + sharedLevels: levels, + fileCount: len(meta) + len(sharedMeta) + len(externalMeta), + } + return result, nil } // Struct for sorting metadatas by smallest user keys, while ensuring the @@ -232,32 +460,79 @@ func (m metaAndPaths) Less(i, j int) bool { func (m metaAndPaths) Swap(i, j int) { m.meta[i], m.meta[j] = m.meta[j], m.meta[i] - m.paths[i], m.paths[j] = m.paths[j], m.paths[i] + if m.paths != nil { + m.paths[i], m.paths[j] = m.paths[j], m.paths[i] + } } -func ingestSortAndVerify(cmp Compare, meta []*fileMetadata, paths []string) error { - if len(meta) <= 1 { +func ingestSortAndVerify(cmp Compare, lr ingestLoadResult, exciseSpan KeyRange) error { + // Verify that all the shared files (i.e. files in sharedMeta) + // fit within the exciseSpan. + for i := range lr.sharedMeta { + f := lr.sharedMeta[i] + if !exciseSpan.Contains(cmp, f.Smallest) || !exciseSpan.Contains(cmp, f.Largest) { + return errors.AssertionFailedf("pebble: shared file outside of excise span, span [%s-%s), file = %s", exciseSpan.Start, exciseSpan.End, f.String()) + } + } + if len(lr.externalMeta) > 0 { + if len(lr.localMeta) > 0 || len(lr.sharedMeta) > 0 { + // Currently we only support external ingests on their own. If external + // files are present alongside local/shared files, return an error. + return errors.AssertionFailedf("pebble: external files cannot be ingested atomically alongside other types of files") + } + sort.Sort(&metaAndPaths{ + meta: lr.externalMeta, + cmp: cmp, + }) + for i := 1; i < len(lr.externalMeta); i++ { + if sstableKeyCompare(cmp, lr.externalMeta[i-1].Largest, lr.externalMeta[i].Smallest) >= 0 { + return errors.AssertionFailedf("pebble: external sstables have overlapping ranges") + } + } + return nil + } + if len(lr.localMeta) <= 1 || len(lr.localPaths) <= 1 { return nil } sort.Sort(&metaAndPaths{ - meta: meta, - paths: paths, + meta: lr.localMeta, + paths: lr.localPaths, cmp: cmp, }) - for i := 1; i < len(meta); i++ { - if sstableKeyCompare(cmp, meta[i-1].Largest, meta[i].Smallest) >= 0 { - return errors.New("pebble: external sstables have overlapping ranges") + for i := 1; i < len(lr.localPaths); i++ { + if sstableKeyCompare(cmp, lr.localMeta[i-1].Largest, lr.localMeta[i].Smallest) >= 0 { + return errors.AssertionFailedf("pebble: local ingestion sstables have overlapping ranges") + } + } + if len(lr.sharedMeta) == 0 { + return nil + } + filesInLevel := make([]*fileMetadata, 0, len(lr.sharedMeta)) + for l := sharedLevelsStart; l < numLevels; l++ { + filesInLevel = filesInLevel[:0] + for i := range lr.sharedMeta { + if lr.sharedLevels[i] == uint8(l) { + filesInLevel = append(filesInLevel, lr.sharedMeta[i]) + } + } + sort.Slice(filesInLevel, func(i, j int) bool { + return cmp(filesInLevel[i].Smallest.UserKey, filesInLevel[j].Smallest.UserKey) < 0 + }) + for i := 1; i < len(filesInLevel); i++ { + if sstableKeyCompare(cmp, filesInLevel[i-1].Largest, filesInLevel[i].Smallest) >= 0 { + return errors.AssertionFailedf("pebble: external shared sstables have overlapping ranges") + } } } return nil } -func ingestCleanup(objProvider *objstorage.Provider, meta []*fileMetadata) error { +func ingestCleanup(objProvider objstorage.Provider, meta []*fileMetadata) error { var firstErr error for i := range meta { - if err := objProvider.Remove(fileTypeTable, meta[i].FileNum); err != nil { + if err := objProvider.Remove(fileTypeTable, meta[i].FileBacking.DiskFileNum); err != nil { firstErr = firstError(firstErr, err) } } @@ -265,14 +540,21 @@ func ingestCleanup(objProvider *objstorage.Provider, meta []*fileMetadata) error } // ingestLink creates new objects which are backed by either hardlinks to or -// copies of the ingested files. +// copies of the ingested files. It also attaches shared objects to the provider. func ingestLink( - jobID int, opts *Options, objProvider *objstorage.Provider, paths []string, meta []*fileMetadata, + jobID int, + opts *Options, + objProvider objstorage.Provider, + lr ingestLoadResult, + shared []SharedSSTMeta, ) error { - for i := range paths { - objMeta, err := objProvider.LinkOrCopyFromLocal(opts.FS, paths[i], fileTypeTable, meta[i].FileNum) + for i := range lr.localPaths { + objMeta, err := objProvider.LinkOrCopyFromLocal( + context.TODO(), opts.FS, lr.localPaths[i], fileTypeTable, lr.localMeta[i].FileBacking.DiskFileNum, + objstorage.CreateOptions{PreferSharedStorage: true}, + ) if err != nil { - if err2 := ingestCleanup(objProvider, meta[:i]); err2 != nil { + if err2 := ingestCleanup(objProvider, lr.localMeta[:i]); err2 != nil { opts.Logger.Infof("ingest cleanup failed: %v", err2) } return err @@ -282,15 +564,57 @@ func ingestLink( JobID: jobID, Reason: "ingesting", Path: objProvider.Path(objMeta), - FileNum: meta[i].FileNum, + FileNum: lr.localMeta[i].FileNum, }) } } + sharedObjs := make([]objstorage.RemoteObjectToAttach, 0, len(shared)) + for i := range shared { + backing, err := shared[i].Backing.Get() + if err != nil { + return err + } + sharedObjs = append(sharedObjs, objstorage.RemoteObjectToAttach{ + FileNum: lr.sharedMeta[i].FileBacking.DiskFileNum, + FileType: fileTypeTable, + Backing: backing, + }) + } + sharedObjMetas, err := objProvider.AttachRemoteObjects(sharedObjs) + if err != nil { + return err + } + for i := range sharedObjMetas { + // One corner case around file sizes we need to be mindful of, is that + // if one of the shareObjs was initially created by us (and has boomeranged + // back from another node), we'll need to update the FileBacking's size + // to be the true underlying size. Otherwise, we could hit errors when we + // open the db again after a crash/restart (see checkConsistency in open.go), + // plus it more accurately allows us to prioritize compactions of files + // that were originally created by us. + if sharedObjMetas[i].IsShared() && !objProvider.IsSharedForeign(sharedObjMetas[i]) { + size, err := objProvider.Size(sharedObjMetas[i]) + if err != nil { + return err + } + lr.sharedMeta[i].FileBacking.Size = uint64(size) + } + if opts.EventListener.TableCreated != nil { + opts.EventListener.TableCreated(TableCreateInfo{ + JobID: jobID, + Reason: "ingesting", + Path: objProvider.Path(sharedObjMetas[i]), + FileNum: lr.sharedMeta[i].FileNum, + }) + } + } + // We do not need to do anything about lr.externalMetas. Those were already + // linked in ingestLoad. return nil } -func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bool { +func ingestMemtableOverlaps(cmp Compare, mem flushable, keyRanges []internalKeyRange) bool { iter := mem.newIter(nil) rangeDelIter := mem.newRangeDelIter(nil) rkeyIter := mem.newRangeKeyIter(nil) @@ -306,8 +630,8 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo return err } - for _, m := range meta { - if overlapWithIterator(iter, &rangeDelIter, rkeyIter, m, cmp) { + for _, kr := range keyRanges { + if overlapWithIterator(iter, &rangeDelIter, rkeyIter, kr, cmp) { closeIters() return true } @@ -318,12 +642,12 @@ func ingestMemtableOverlaps(cmp Compare, mem flushable, meta []*fileMetadata) bo } func ingestUpdateSeqNum( - cmp Compare, format base.FormatKey, seqNum uint64, meta []*fileMetadata, + cmp Compare, format base.FormatKey, seqNum uint64, loadResult ingestLoadResult, ) error { setSeqFn := func(k base.InternalKey) base.InternalKey { return base.MakeInternalKey(k.UserKey, seqNum, k.Kind()) } - for _, m := range meta { + updateMetadata := func(m *fileMetadata) error { // NB: we set the fields directly here, rather than via their Extend* // methods, as we are updating sequence numbers. if m.HasPointKeys { @@ -355,25 +679,55 @@ func ingestUpdateSeqNum( return err } seqNum++ + return nil + } + + // Shared sstables are required to be sorted by level ascending. We then + // iterate the shared sstables in reverse, assigning the lower sequence + // numbers to the shared sstables that will be ingested into the lower + // (larger numbered) levels first. This ensures sequence number shadowing is + // correct. + for i := len(loadResult.sharedMeta) - 1; i >= 0; i-- { + if i-1 >= 0 && loadResult.sharedLevels[i-1] > loadResult.sharedLevels[i] { + panic(errors.AssertionFailedf("shared files %s, %s out of order", loadResult.sharedMeta[i-1], loadResult.sharedMeta[i])) + } + if err := updateMetadata(loadResult.sharedMeta[i]); err != nil { + return err + } + } + for i := range loadResult.localMeta { + if err := updateMetadata(loadResult.localMeta[i]); err != nil { + return err + } + } + for i := range loadResult.externalMeta { + if err := updateMetadata(loadResult.externalMeta[i]); err != nil { + return err + } } return nil } +// Denotes an internal key range. Smallest and largest are both inclusive. +type internalKeyRange struct { + smallest, largest InternalKey +} + func overlapWithIterator( iter internalIterator, rangeDelIter *keyspan.FragmentIterator, rkeyIter keyspan.FragmentIterator, - meta *fileMetadata, + keyRange internalKeyRange, cmp Compare, ) bool { // Check overlap with point operations. // // When using levelIter, it seeks to the SST whose boundaries - // contain meta.Smallest.UserKey(S). + // contain keyRange.smallest.UserKey(S). // It then tries to find a point in that SST that is >= S. // If there's no such point it means the SST ends in a tombstone in which case // levelIter.SeekGE generates a boundary range del sentinel. - // The comparison of this boundary with meta.Largest(L) below + // The comparison of this boundary with keyRange.largest(L) below // is subtle but maintains correctness. // 1) boundary < L, // since boundary is also > S (initial seek), @@ -385,9 +739,9 @@ func overlapWithIterator( // means boundary < L and hence is similar to 1). // 4) boundary == L and L is sentinel, // we'll always overlap since for any values of i,j ranges [i, k) and [j, k) always overlap. - key, _ := iter.SeekGE(meta.Smallest.UserKey, base.SeekGEFlagsNone) + key, _ := iter.SeekGE(keyRange.smallest.UserKey, base.SeekGEFlagsNone) if key != nil { - c := sstableKeyCompare(cmp, *key, meta.Largest) + c := sstableKeyCompare(cmp, *key, keyRange.largest) if c <= 0 { return true } @@ -399,7 +753,7 @@ func overlapWithIterator( computeOverlapWithSpans := func(rIter keyspan.FragmentIterator) bool { // NB: The spans surfaced by the fragment iterator are non-overlapping. - span := rIter.SeekLT(meta.Smallest.UserKey) + span := rIter.SeekLT(keyRange.smallest.UserKey) if span == nil { span = rIter.Next() } @@ -408,13 +762,13 @@ func overlapWithIterator( continue } key := span.SmallestKey() - c := sstableKeyCompare(cmp, key, meta.Largest) + c := sstableKeyCompare(cmp, key, keyRange.largest) if c > 0 { // The start of the span is after the largest key in the // ingested table. return false } - if cmp(span.End, meta.Smallest.UserKey) > 0 { + if cmp(span.End, keyRange.smallest.UserKey) > 0 { // The end of the span is greater than the smallest in the // table. Note that the span end key is exclusive, thus ">0" // instead of ">=0". @@ -443,16 +797,21 @@ func overlapWithIterator( return computeOverlapWithSpans(*rangeDelIter) } +// ingestTargetLevel returns the target level for a file being ingested. +// If suggestSplit is true, it accounts for ingest-time splitting as part of +// its target level calculation, and if a split candidate is found, that file +// is returned as the splitFile. func ingestTargetLevel( newIters tableNewIters, newRangeKeyIter keyspan.TableNewSpanIter, iterOps IterOptions, - cmp Compare, + comparer *Comparer, v *version, baseLevel int, compactions map[*compaction]struct{}, meta *fileMetadata, -) (int, error) { + suggestSplit bool, +) (targetLevel int, splitFile *fileMetadata, err error) { // Find the lowest level which does not have any files which overlap meta. We // search from L0 to L6 looking for whether there are any files in the level // which overlap meta. We want the "lowest" level (where lower means @@ -467,6 +826,14 @@ func ingestTargetLevel( // violate the sequence number invariant. // - no file boundary overlap with level i, since that will violate the // invariant that files do not overlap in levels i > 0. + // - if there is only a file overlap at a given level, and no data overlap, + // we can still slot a file at that level. We return the fileMetadata with + // which we have file boundary overlap (must be only one file, as sstable + // bounds are usually tight on user keys) and the caller is expected to split + // that sstable into two virtual sstables, allowing this file to go into that + // level. Note that if we have file boundary overlap with two files, which + // should only happen on rare occasions, we treat it as data overlap and + // don't use this optimization. // // The file boundary overlap check is simpler to conceptualize. Consider the // following example, in which the ingested file lies completely before or @@ -511,48 +878,46 @@ func ingestTargetLevel( // existing point that falls within the ingested table bounds as being "data // overlap". - targetLevel := 0 + // This assertion implicitly checks that we have the current version of + // the metadata. + if v.L0Sublevels == nil { + return 0, nil, errors.AssertionFailedf("could not read L0 sublevels") + } + // Check for overlap over the keys of L0 by iterating over the sublevels. + for subLevel := 0; subLevel < len(v.L0SublevelFiles); subLevel++ { + iter := newLevelIter(iterOps, comparer, newIters, + v.L0Sublevels.Levels[subLevel].Iter(), manifest.Level(0), internalIterOpts{}) - // Do we overlap with keys in L0? - // TODO(bananabrick): Use sublevels to compute overlap. - iter := v.Levels[0].Iter() - for meta0 := iter.First(); meta0 != nil; meta0 = iter.Next() { - c1 := sstableKeyCompare(cmp, meta.Smallest, meta0.Largest) - c2 := sstableKeyCompare(cmp, meta.Largest, meta0.Smallest) - if c1 > 0 || c2 < 0 { - continue - } + var rangeDelIter keyspan.FragmentIterator + // Pass in a non-nil pointer to rangeDelIter so that levelIter.findFileGE + // sets it up for the target file. + iter.initRangeDel(&rangeDelIter) - // TODO(sumeer): ingest is a user-facing operation, so we should accept a - // context and plumb it through, for tracing. - iter, rangeDelIter, err := newIters(context.Background(), meta0, nil, internalIterOpts{}) + levelIter := keyspan.LevelIter{} + levelIter.Init( + keyspan.SpanIterOptions{}, comparer.Compare, newRangeKeyIter, + v.L0Sublevels.Levels[subLevel].Iter(), manifest.Level(0), manifest.KeyTypeRange, + ) + + kr := internalKeyRange{ + smallest: meta.Smallest, + largest: meta.Largest, + } + overlap := overlapWithIterator(iter, &rangeDelIter, &levelIter, kr, comparer.Compare) + err := iter.Close() // Closes range del iter as well. + err = firstError(err, levelIter.Close()) if err != nil { - return 0, err - } - rkeyIter, err := newRangeKeyIter(meta0, nil) - if err != nil { - return 0, err - } - overlap := overlapWithIterator(iter, &rangeDelIter, rkeyIter, meta, cmp) - err = firstError(err, iter.Close()) - if rangeDelIter != nil { - err = firstError(err, rangeDelIter.Close()) - } - if rkeyIter != nil { - err = firstError(err, rkeyIter.Close()) - } - if err != nil { - return 0, err + return 0, nil, err } if overlap { - return targetLevel, nil + return targetLevel, nil, nil } } level := baseLevel for ; level < numLevels; level++ { - levelIter := newLevelIter(iterOps, cmp, nil /* split */, newIters, - v.Levels[level].Iter(), manifest.Level(level), nil) + levelIter := newLevelIter(iterOps, comparer, newIters, + v.Levels[level].Iter(), manifest.Level(level), internalIterOpts{}) var rangeDelIter keyspan.FragmentIterator // Pass in a non-nil pointer to rangeDelIter so that levelIter.findFileGE // sets it up for the target file. @@ -560,50 +925,76 @@ func ingestTargetLevel( rkeyLevelIter := &keyspan.LevelIter{} rkeyLevelIter.Init( - keyspan.SpanIterOptions{}, cmp, newRangeKeyIter, + keyspan.SpanIterOptions{}, comparer.Compare, newRangeKeyIter, v.Levels[level].Iter(), manifest.Level(level), manifest.KeyTypeRange, ) - overlap := overlapWithIterator(levelIter, &rangeDelIter, rkeyLevelIter, meta, cmp) + kr := internalKeyRange{ + smallest: meta.Smallest, + largest: meta.Largest, + } + overlap := overlapWithIterator(levelIter, &rangeDelIter, rkeyLevelIter, kr, comparer.Compare) err := levelIter.Close() // Closes range del iter as well. err = firstError(err, rkeyLevelIter.Close()) if err != nil { - return 0, err + return 0, nil, err } if overlap { - return targetLevel, nil + return targetLevel, splitFile, nil } // Check boundary overlap. - boundaryOverlaps := v.Overlaps(level, cmp, meta.Smallest.UserKey, + var candidateSplitFile *fileMetadata + boundaryOverlaps := v.Overlaps(level, comparer.Compare, meta.Smallest.UserKey, meta.Largest.UserKey, meta.Largest.IsExclusiveSentinel()) if !boundaryOverlaps.Empty() { - continue + // We are already guaranteed to not have any data overlaps with files + // in boundaryOverlaps, otherwise we'd have returned in the above if + // statements. Use this, plus boundaryOverlaps.Len() == 1 to detect for + // the case where we can slot this file into the current level despite + // a boundary overlap, by splitting one existing file into two virtual + // sstables. + if suggestSplit && boundaryOverlaps.Len() == 1 { + iter := boundaryOverlaps.Iter() + candidateSplitFile = iter.First() + } else { + // We either don't want to suggest ingest-time splits (i.e. + // !suggestSplit), or we boundary-overlapped with more than one file. + continue + } } - // Check boundary overlap with any ongoing compactions. + // Check boundary overlap with any ongoing compactions. We consider an + // overlapping compaction that's writing files to an output level as + // equivalent to boundary overlap with files in that output level. // - // We cannot check for data overlap with the new SSTs compaction will - // produce since compaction hasn't been done yet. However, there's no need - // to check since all keys in them will either be from c.startLevel or - // c.outputLevel, both levels having their data overlap already tested - // negative (else we'd have returned earlier). + // We cannot check for data overlap with the new SSTs compaction will produce + // since compaction hasn't been done yet. However, there's no need to check + // since all keys in them will be from levels in [c.startLevel, + // c.outputLevel], and all those levels have already had their data overlap + // tested negative (else we'd have returned earlier). + // + // An alternative approach would be to cancel these compactions and proceed + // with an ingest-time split on this level if necessary. However, compaction + // cancellation can result in significant wasted effort and is best avoided + // unless necessary. overlaps := false for c := range compactions { if c.outputLevel == nil || level != c.outputLevel.level { continue } - if cmp(meta.Smallest.UserKey, c.largest.UserKey) <= 0 && - cmp(meta.Largest.UserKey, c.smallest.UserKey) >= 0 { + if comparer.Compare(meta.Smallest.UserKey, c.largest.UserKey) <= 0 && + comparer.Compare(meta.Largest.UserKey, c.smallest.UserKey) >= 0 { overlaps = true break } } if !overlaps { targetLevel = level + splitFile = candidateSplitFile } } - return targetLevel, nil + return targetLevel, splitFile, nil } // Ingest ingests a set of sstables into the DB. Ingestion of the files is @@ -613,6 +1004,13 @@ func ingestTargetLevel( // the same filesystem as the DB. Sstables can be created for ingestion using // sstable.Writer. On success, Ingest removes the input paths. // +// Two types of sstables are accepted for ingestion(s): one is sstables present +// in the instance's vfs.FS and can be referenced locally. The other is sstables +// present in remote.Storage, referred to as shared or foreign sstables. These +// shared sstables can be linked through objstorageprovider.Provider, and do not +// need to already be present on the local vfs.FS. Foreign sstables must all fit +// in an excise span, and are destined for a level specified in SharedSSTMeta. +// // All sstables *must* be Sync()'d by the caller after all bytes are written // and before its file handle is closed; failure to do so could violate // durability or lead to corrupted on-disk state. This method cannot, in a @@ -624,21 +1022,28 @@ func ingestTargetLevel( // Ingestion loads each sstable into the lowest level of the LSM which it // doesn't overlap (see ingestTargetLevel). If an sstable overlaps a memtable, // ingestion forces the memtable to flush, and then waits for the flush to -// occur. +// occur. In some cases, such as with no foreign sstables and no excise span, +// ingestion that gets blocked on a memtable can join the flushable queue and +// finish even before the memtable has been flushed. // // The steps for ingestion are: // // 1. Allocate file numbers for every sstable being ingested. -// 2. Load the metadata for all sstables being ingest. -// 3. Sort the sstables by smallest key, verifying non overlap. -// 4. Hard link (or copy) the sstables into the DB directory. +// 2. Load the metadata for all sstables being ingested. +// 3. Sort the sstables by smallest key, verifying non overlap (for local +// sstables). +// 4. Hard link (or copy) the local sstables into the DB directory. // 5. Allocate a sequence number to use for all of the entries in the -// sstables. This is the step where overlap with memtables is +// local sstables. This is the step where overlap with memtables is // determined. If there is overlap, we remember the most recent memtable // that overlaps. -// 6. Update the sequence number in the ingested sstables. +// 6. Update the sequence number in the ingested local sstables. (Remote +// sstables get fixed sequence numbers that were determined at load time.) // 7. Wait for the most recent memtable that overlaps to flush (if any). // 8. Add the ingested sstables to the version (DB.ingestApply). +// 8.1. If an excise span was specified, figure out what sstables in the +// current version overlap with the excise span, and create new virtual +// sstables out of those sstables that exclude the excised span (DB.excise). // 9. Publish the ingestion sequence number. // // Note that if the mutable memtable overlaps with ingestion, a flush of the @@ -655,7 +1060,7 @@ func (d *DB) Ingest(paths []string) error { if d.opts.ReadOnly { return ErrReadOnly } - _, err := d.ingest(paths, ingestTargetLevel) + _, err := d.ingest(paths, ingestTargetLevel, nil /* shared */, KeyRange{}, nil /* external */) return err } @@ -675,6 +1080,32 @@ type IngestOperationStats struct { MemtableOverlappingFiles int } +// ExternalFile are external sstables that can be referenced through +// objprovider and ingested as remote files that will not be refcounted or +// cleaned up. For use with online restore. Note that the underlying sstable +// could contain keys outside the [Smallest,Largest) bounds; however Pebble +// is expected to only read the keys within those bounds. +type ExternalFile struct { + // Locator is the shared.Locator that can be used with objProvider to + // resolve a reference to this external sstable. + Locator remote.Locator + // ObjName is the unique name of this sstable on Locator. + ObjName string + // Size of the referenced proportion of the virtualized sstable. An estimate + // is acceptable in lieu of the backing file size. + Size uint64 + // SmallestUserKey and LargestUserKey are the [smallest,largest) user key + // bounds of the sstable. Both these bounds are loose i.e. it's possible for + // the sstable to not span the entirety of this range. However, multiple + // ExternalFiles in one ingestion must all have non-overlapping + // [smallest, largest) spans. Note that this Largest bound is exclusive. + SmallestUserKey, LargestUserKey []byte + // HasPointKey and HasRangeKey denote whether this file contains point keys + // or range keys. If both structs are false, an error is returned during + // ingestion. + HasPointKey, HasRangeKey bool +} + // IngestWithStats does the same as Ingest, and additionally returns // IngestOperationStats. func (d *DB) IngestWithStats(paths []string) (IngestOperationStats, error) { @@ -684,7 +1115,47 @@ func (d *DB) IngestWithStats(paths []string) (IngestOperationStats, error) { if d.opts.ReadOnly { return IngestOperationStats{}, ErrReadOnly } - return d.ingest(paths, ingestTargetLevel) + return d.ingest(paths, ingestTargetLevel, nil /* shared */, KeyRange{}, nil /* external */) +} + +// IngestExternalFiles does the same as IngestWithStats, and additionally +// accepts external files (with locator info that can be resolved using +// d.opts.SharedStorage). These files must also be non-overlapping with +// each other, and must be resolvable through d.objProvider. +func (d *DB) IngestExternalFiles(external []ExternalFile) (IngestOperationStats, error) { + if err := d.closed.Load(); err != nil { + panic(err) + } + + if d.opts.ReadOnly { + return IngestOperationStats{}, ErrReadOnly + } + if d.opts.Experimental.RemoteStorage == nil { + return IngestOperationStats{}, errors.New("pebble: cannot ingest external files without shared storage configured") + } + return d.ingest(nil, ingestTargetLevel, nil /* shared */, KeyRange{}, external) +} + +// IngestAndExcise does the same as IngestWithStats, and additionally accepts a +// list of shared files to ingest that can be read from a remote.Storage through +// a Provider. All the shared files must live within exciseSpan, and any existing +// keys in exciseSpan are deleted by turning existing sstables into virtual +// sstables (if not virtual already) and shrinking their spans to exclude +// exciseSpan. See the comment at Ingest for a more complete picture of the +// ingestion process. +// +// Panics if this DB instance was not instantiated with a remote.Storage and +// shared sstables are present. +func (d *DB) IngestAndExcise( + paths []string, shared []SharedSSTMeta, exciseSpan KeyRange, +) (IngestOperationStats, error) { + if err := d.closed.Load(); err != nil { + panic(err) + } + if d.opts.ReadOnly { + return IngestOperationStats{}, ErrReadOnly + } + return d.ingest(paths, ingestTargetLevel, shared, exciseSpan, nil /* external */) } // Both DB.mu and commitPipeline.mu must be held while this is called. @@ -700,12 +1171,12 @@ func (d *DB) newIngestedFlushableEntry( // time, then we'll lose the ingest sequence number information. But this // information will also be reconstructed on node restart. if err := ingestUpdateSeqNum( - d.cmp, d.opts.Comparer.FormatKey, seqNum, meta, + d.cmp, d.opts.Comparer.FormatKey, seqNum, ingestLoadResult{localMeta: meta}, ); err != nil { return nil, err } - f := newIngestedFlushable(meta, d.cmp, d.split, d.newIters, d.tableNewRangeKeyIter) + f := newIngestedFlushable(meta, d.opts.Comparer, d.newIters, d.tableNewRangeKeyIter) // NB: The logNum/seqNum are the WAL number which we're writing this entry // to and the sequence number within the WAL which we'll write this entry @@ -714,13 +1185,13 @@ func (d *DB) newIngestedFlushableEntry( // The flushable entry starts off with a single reader ref, so increment // the FileMetadata.Refs. for _, file := range f.files { - atomic.AddInt32(&file.Refs, 1) + file.Ref() } - entry.unrefFiles = func() []*fileMetadata { - var obsolete []*fileMetadata + entry.unrefFiles = func() []*fileBacking { + var obsolete []*fileBacking for _, file := range f.files { - if val := atomic.AddInt32(&file.Refs, -1); val == 0 { - obsolete = append(obsolete, file) + if file.Unref() == 0 { + obsolete = append(obsolete, file.FileMetadata.FileBacking) } } return obsolete @@ -794,36 +1265,49 @@ func (d *DB) handleIngestAsFlushable(meta []*fileMetadata, seqNum uint64) error return nil } +// See comment at Ingest() for details on how this works. func (d *DB) ingest( - paths []string, targetLevelFunc ingestTargetLevelFunc, + paths []string, + targetLevelFunc ingestTargetLevelFunc, + shared []SharedSSTMeta, + exciseSpan KeyRange, + external []ExternalFile, ) (IngestOperationStats, error) { + if len(shared) > 0 && d.opts.Experimental.RemoteStorage == nil { + panic("cannot ingest shared sstables with nil SharedStorage") + } + if (exciseSpan.Valid() || len(shared) > 0 || len(external) > 0) && d.FormatMajorVersion() < FormatVirtualSSTables { + return IngestOperationStats{}, errors.New("pebble: format major version too old for excise, shared or external sstable ingestion") + } // Allocate file numbers for all of the files being ingested and mark them as // pending in order to prevent them from being deleted. Note that this causes // the file number ordering to be out of alignment with sequence number // ordering. The sorting of L0 tables by sequence number avoids relying on // that (busted) invariant. d.mu.Lock() - pendingOutputs := make([]FileNum, len(paths)) - for i := range paths { - pendingOutputs[i] = d.mu.versions.getNextFileNum() + pendingOutputs := make([]base.DiskFileNum, len(paths)+len(shared)+len(external)) + for i := 0; i < len(paths)+len(shared)+len(external); i++ { + pendingOutputs[i] = d.mu.versions.getNextFileNum().DiskFileNum() } + jobID := d.mu.nextJobID d.mu.nextJobID++ d.mu.Unlock() - // Load the metadata for all of the files being ingested. This step detects + // Load the metadata for all the files being ingested. This step detects // and elides empty sstables. - meta, paths, err := ingestLoad(d.opts, d.FormatMajorVersion(), paths, d.cacheID, pendingOutputs) + loadResult, err := ingestLoad(d.opts, d.FormatMajorVersion(), paths, shared, external, d.cacheID, pendingOutputs, d.objProvider, jobID) if err != nil { return IngestOperationStats{}, err } - if len(meta) == 0 { + + if loadResult.fileCount == 0 { // All of the sstables to be ingested were empty. Nothing to do. return IngestOperationStats{}, nil } // Verify the sstables do not overlap. - if err := ingestSortAndVerify(d.cmp, meta, paths); err != nil { + if err := ingestSortAndVerify(d.cmp, loadResult, exciseSpan); err != nil { return IngestOperationStats{}, err } @@ -832,9 +1316,10 @@ func (d *DB) ingest( // (e.g. because the files reside on a different filesystem), ingestLink will // fall back to copying, and if that fails we undo our work and return an // error. - if err := ingestLink(jobID, d.opts, d.objProvider, paths, meta); err != nil { + if err := ingestLink(jobID, d.opts, d.objProvider, loadResult, shared); err != nil { return IngestOperationStats{}, err } + // Make the new tables durable. We need to do this at some point before we // update the MANIFEST (via logAndApply), otherwise a crash can have the // tables referenced in the MANIFEST, but not present in the provider. @@ -845,7 +1330,7 @@ func (d *DB) ingest( // metaFlushableOverlaps is a slice parallel to meta indicating which of the // ingested sstables overlap some table in the flushable queue. It's used to // approximate ingest-into-L0 stats when using flushable ingests. - metaFlushableOverlaps := make([]bool, len(meta)) + metaFlushableOverlaps := make([]bool, loadResult.fileCount) var mem *flushableEntry var mut *memTable // asFlushable indicates whether the sstable was ingested as a flushable. @@ -866,12 +1351,17 @@ func (d *DB) ingest( iter := m.newIter(nil) rangeDelIter := m.newRangeDelIter(nil) rkeyIter := m.newRangeKeyIter(nil) - for i := range meta { + + checkForOverlap := func(i int, meta *fileMetadata) { if metaFlushableOverlaps[i] { // This table already overlapped a more recent flushable. - continue + return } - if overlapWithIterator(iter, &rangeDelIter, rkeyIter, meta[i], d.cmp) { + kr := internalKeyRange{ + smallest: meta.Smallest, + largest: meta.Largest, + } + if overlapWithIterator(iter, &rangeDelIter, rkeyIter, kr, d.cmp) { // If this is the first table to overlap a flushable, save // the flushable. This ingest must be ingested or flushed // after it. @@ -881,6 +1371,26 @@ func (d *DB) ingest( metaFlushableOverlaps[i] = true } } + for i := range loadResult.localMeta { + checkForOverlap(i, loadResult.localMeta[i]) + } + for i := range loadResult.sharedMeta { + checkForOverlap(len(loadResult.localMeta)+i, loadResult.sharedMeta[i]) + } + for i := range loadResult.externalMeta { + checkForOverlap(len(loadResult.localMeta)+len(loadResult.sharedMeta)+i, loadResult.externalMeta[i]) + } + if exciseSpan.Valid() { + kr := internalKeyRange{ + smallest: base.MakeInternalKey(exciseSpan.Start, InternalKeySeqNumMax, InternalKeyKindMax), + largest: base.MakeExclusiveSentinelKey(InternalKeyKindRangeDelete, exciseSpan.End), + } + if overlapWithIterator(iter, &rangeDelIter, rkeyIter, kr, d.cmp) { + if mem == nil { + mem = m + } + } + } err := iter.Close() if rangeDelIter != nil { err = firstError(err, rangeDelIter.Close()) @@ -911,9 +1421,16 @@ func (d *DB) ingest( // The ingestion overlaps with some entry in the flushable queue. if d.FormatMajorVersion() < FormatFlushableIngest || d.opts.Experimental.DisableIngestAsFlushable() || + len(shared) > 0 || exciseSpan.Valid() || len(external) > 0 || (len(d.mu.mem.queue) > d.opts.MemTableStopWritesThreshold-1) { // We're not able to ingest as a flushable, // so we must synchronously flush. + // + // TODO(bilal): Currently, if any of the files being ingested are shared or + // there's an excise span present, we cannot use flushable ingests and need + // to wait synchronously. Either remove this caveat by fleshing out + // flushable ingest logic to also account for these cases, or remove this + // comment. Tracking issue: https://github.com/cockroachdb/pebble/issues/2676 if mem.flushable == d.mu.mem.mutable { err = d.makeRoomForWrite(nil) } @@ -933,7 +1450,7 @@ func (d *DB) ingest( // Since there aren't too many memtables already queued up, we can // slide the ingested sstables on top of the existing memtables. asFlushable = true - err = d.handleIngestAsFlushable(meta, seqNum) + err = d.handleIngestAsFlushable(loadResult.localMeta, seqNum) } var ve *versionEdit @@ -950,12 +1467,12 @@ func (d *DB) ingest( return } - // Update the sequence number for all of the sstables in the - // metadata. Writing the metadata to the manifest when the - // version edit is applied is the mechanism that persists the - // sequence number. The sstables themselves are left unmodified. + // Update the sequence numbers for all ingested sstables' + // metadata. When the version edit is applied, the metadata is + // written to the manifest, persisting the sequence number. + // The sstables themselves are left unmodified. if err = ingestUpdateSeqNum( - d.cmp, d.opts.Comparer.FormatKey, seqNum, meta, + d.cmp, d.opts.Comparer.FormatKey, seqNum, loadResult, ); err != nil { if mut != nil { if mut.writerUnref() { @@ -975,30 +1492,49 @@ func (d *DB) ingest( // Assign the sstables to the correct level in the LSM and apply the // version edit. - ve, err = d.ingestApply(jobID, meta, targetLevelFunc, mut) + ve, err = d.ingestApply(jobID, loadResult, targetLevelFunc, mut, exciseSpan) } - d.commit.AllocateSeqNum(len(meta), prepare, apply) + // Only one ingest can occur at a time because if not, one would block waiting + // for the other to finish applying. This blocking would happen while holding + // the commit mutex which would prevent unrelated batches from writing their + // changes to the WAL and memtable. This will cause a bigger commit hiccup + // during ingestion. + d.commit.ingestSem <- struct{}{} + d.commit.AllocateSeqNum(loadResult.fileCount, prepare, apply) + <-d.commit.ingestSem if err != nil { - if err2 := ingestCleanup(d.objProvider, meta); err2 != nil { + if err2 := ingestCleanup(d.objProvider, loadResult.localMeta); err2 != nil { d.opts.Logger.Infof("ingest cleanup failed: %v", err2) } } else { // Since we either created a hard link to the ingesting files, or copied // them over, it is safe to remove the originals paths. - for _, path := range paths { + for _, path := range loadResult.localPaths { if err2 := d.opts.FS.Remove(path); err2 != nil { d.opts.Logger.Infof("ingest failed to remove original file: %s", err2) } } } + if invariants.Enabled { + for _, sharedMeta := range loadResult.sharedMeta { + d.checkVirtualBounds(sharedMeta) + } + } + info := TableIngestInfo{ - JobID: jobID, - GlobalSeqNum: meta[0].SmallestSeqNum, - Err: err, - flushable: asFlushable, + JobID: jobID, + Err: err, + flushable: asFlushable, + } + if len(loadResult.localMeta) > 0 { + info.GlobalSeqNum = loadResult.localMeta[0].SmallestSeqNum + } else if len(loadResult.sharedMeta) > 0 { + info.GlobalSeqNum = loadResult.sharedMeta[0].SmallestSeqNum + } else { + info.GlobalSeqNum = loadResult.externalMeta[0].SmallestSeqNum } var stats IngestOperationStats if ve != nil { @@ -1014,16 +1550,17 @@ func (d *DB) ingest( if e.Level == 0 { stats.ApproxIngestedIntoL0Bytes += e.Meta.Size } - if metaFlushableOverlaps[i] { + if i < len(metaFlushableOverlaps) && metaFlushableOverlaps[i] { stats.MemtableOverlappingFiles++ } } } else if asFlushable { + // NB: If asFlushable == true, there are no shared sstables. info.Tables = make([]struct { TableInfo Level int - }, len(meta)) - for i, f := range meta { + }, len(loadResult.localMeta)) + for i, f := range loadResult.localMeta { info.Tables[i].Level = -1 info.Tables[i].TableInfo = f.TableInfo() stats.Bytes += f.Size @@ -1046,25 +1583,422 @@ func (d *DB) ingest( return stats, err } +// excise updates ve to include a replacement of the file m with new virtual +// sstables that exclude exciseSpan, returning a slice of newly-created files if +// any. If the entirety of m is deleted by exciseSpan, no new sstables are added +// and m is deleted. Note that ve is updated in-place. +// +// The manifest lock must be held when calling this method. +func (d *DB) excise( + exciseSpan KeyRange, m *fileMetadata, ve *versionEdit, level int, +) ([]manifest.NewFileEntry, error) { + numCreatedFiles := 0 + // Check if there's actually an overlap between m and exciseSpan. + if !exciseSpan.Overlaps(d.cmp, m) { + return nil, nil + } + ve.DeletedFiles[deletedFileEntry{ + Level: level, + FileNum: m.FileNum, + }] = m + // Fast path: m sits entirely within the exciseSpan, so just delete it. + if exciseSpan.Contains(d.cmp, m.Smallest) && exciseSpan.Contains(d.cmp, m.Largest) { + return nil, nil + } + var iter internalIterator + var rangeDelIter keyspan.FragmentIterator + var rangeKeyIter keyspan.FragmentIterator + needsBacking := false + // Create a file to the left of the excise span, if necessary. + // The bounds of this file will be [m.Smallest, lastKeyBefore(exciseSpan.Start)]. + // + // We create bounds that are tight on user keys, and we make the effort to find + // the last key in the original sstable that's smaller than exciseSpan.Start + // even though it requires some sstable reads. We could choose to create + // virtual sstables on loose userKey bounds, in which case we could just set + // leftFile.Largest to an exclusive sentinel at exciseSpan.Start. The biggest + // issue with that approach would be that it'd lead to lots of small virtual + // sstables in the LSM that have no guarantee on containing even a single user + // key within the file bounds. This has the potential to increase both read and + // write-amp as we will be opening up these sstables only to find no relevant + // keys in the read path, and compacting sstables on top of them instead of + // directly into the space occupied by them. We choose to incur the cost of + // calculating tight bounds at this time instead of creating more work in the + // future. + // + // TODO(bilal): Some of this work can happen without grabbing the manifest + // lock; we could grab one currentVersion, release the lock, calculate excised + // files, then grab the lock again and recalculate for just the files that + // have changed since our previous calculation. Do this optimiaztino as part of + // https://github.com/cockroachdb/pebble/issues/2112 . + if d.cmp(m.Smallest.UserKey, exciseSpan.Start) < 0 { + leftFile := &fileMetadata{ + Virtual: true, + FileBacking: m.FileBacking, + FileNum: d.mu.versions.getNextFileNum(), + // Note that these are loose bounds for smallest/largest seqnums, but they're + // sufficient for maintaining correctness. + SmallestSeqNum: m.SmallestSeqNum, + LargestSeqNum: m.LargestSeqNum, + } + if m.HasPointKeys && !exciseSpan.Contains(d.cmp, m.SmallestPointKey) { + // This file will contain point keys + smallestPointKey := m.SmallestPointKey + var err error + iter, rangeDelIter, err = d.newIters(context.TODO(), m, &IterOptions{level: manifest.Level(level)}, internalIterOpts{}) + if err != nil { + return nil, err + } + var key *InternalKey + if iter != nil { + defer iter.Close() + key, _ = iter.SeekLT(exciseSpan.Start, base.SeekLTFlagsNone) + } else { + iter = emptyIter + } + if key != nil { + leftFile.ExtendPointKeyBounds(d.cmp, smallestPointKey, key.Clone()) + } + // Store the min of (exciseSpan.Start, rdel.End) in lastRangeDel. This + // needs to be a copy if the key is owned by the range del iter. + var lastRangeDel []byte + if rangeDelIter != nil { + defer rangeDelIter.Close() + rdel := rangeDelIter.SeekLT(exciseSpan.Start) + if rdel != nil { + lastRangeDel = append(lastRangeDel[:0], rdel.End...) + if d.cmp(lastRangeDel, exciseSpan.Start) > 0 { + lastRangeDel = exciseSpan.Start + } + } + } else { + rangeDelIter = emptyKeyspanIter + } + if lastRangeDel != nil { + leftFile.ExtendPointKeyBounds(d.cmp, smallestPointKey, base.MakeExclusiveSentinelKey(InternalKeyKindRangeDelete, lastRangeDel)) + } + } + if m.HasRangeKeys && !exciseSpan.Contains(d.cmp, m.SmallestRangeKey) { + // This file will contain range keys + var err error + smallestRangeKey := m.SmallestRangeKey + rangeKeyIter, err = d.tableNewRangeKeyIter(m, keyspan.SpanIterOptions{}) + if err != nil { + return nil, err + } + // Store the min of (exciseSpan.Start, rkey.End) in lastRangeKey. This + // needs to be a copy if the key is owned by the range key iter. + var lastRangeKey []byte + var lastRangeKeyKind InternalKeyKind + defer rangeKeyIter.Close() + rkey := rangeKeyIter.SeekLT(exciseSpan.Start) + if rkey != nil { + lastRangeKey = append(lastRangeKey[:0], rkey.End...) + if d.cmp(lastRangeKey, exciseSpan.Start) > 0 { + lastRangeKey = exciseSpan.Start + } + lastRangeKeyKind = rkey.Keys[0].Kind() + } + if lastRangeKey != nil { + leftFile.ExtendRangeKeyBounds(d.cmp, smallestRangeKey, base.MakeExclusiveSentinelKey(lastRangeKeyKind, lastRangeKey)) + } + } + if leftFile.HasRangeKeys || leftFile.HasPointKeys { + var err error + leftFile.Size, err = d.tableCache.estimateSize(m, leftFile.Smallest.UserKey, leftFile.Largest.UserKey) + if err != nil { + return nil, err + } + if leftFile.Size == 0 { + // On occasion, estimateSize gives us a low estimate, i.e. a 0 file size, + // such as if the excised file only has range keys/dels and no point + // keys. This can cause panics in places where we divide by file sizes. + // Correct for it here. + leftFile.Size = 1 + } + if err := leftFile.Validate(d.cmp, d.opts.Comparer.FormatKey); err != nil { + return nil, err + } + leftFile.ValidateVirtual(m) + d.checkVirtualBounds(leftFile) + ve.NewFiles = append(ve.NewFiles, newFileEntry{Level: level, Meta: leftFile}) + needsBacking = true + numCreatedFiles++ + } + } + // Create a file to the right, if necessary. + if exciseSpan.Contains(d.cmp, m.Largest) { + // No key exists to the right of the excise span in this file. + if needsBacking && !m.Virtual { + // If m is virtual, then its file backing is already known to the manifest. + // We don't need to create another file backing. Note that there must be + // only one CreatedBackingTables entry per backing sstable. This is + // indicated by the VersionEdit.CreatedBackingTables invariant. + ve.CreatedBackingTables = append(ve.CreatedBackingTables, m.FileBacking) + } + return ve.NewFiles[len(ve.NewFiles)-numCreatedFiles:], nil + } + // Create a new file, rightFile, between [firstKeyAfter(exciseSpan.End), m.Largest]. + // + // See comment before the definition of leftFile for the motivation behind + // calculating tight user-key bounds. + rightFile := &fileMetadata{ + Virtual: true, + FileBacking: m.FileBacking, + FileNum: d.mu.versions.getNextFileNum(), + // Note that these are loose bounds for smallest/largest seqnums, but they're + // sufficient for maintaining correctness. + SmallestSeqNum: m.SmallestSeqNum, + LargestSeqNum: m.LargestSeqNum, + } + if m.HasPointKeys && !exciseSpan.Contains(d.cmp, m.LargestPointKey) { + // This file will contain point keys + largestPointKey := m.LargestPointKey + var err error + if iter == nil && rangeDelIter == nil { + iter, rangeDelIter, err = d.newIters(context.TODO(), m, &IterOptions{level: manifest.Level(level)}, internalIterOpts{}) + if err != nil { + return nil, err + } + if iter != nil { + defer iter.Close() + } else { + iter = emptyIter + } + if rangeDelIter != nil { + defer rangeDelIter.Close() + } else { + rangeDelIter = emptyKeyspanIter + } + } + key, _ := iter.SeekGE(exciseSpan.End, base.SeekGEFlagsNone) + if key != nil { + rightFile.ExtendPointKeyBounds(d.cmp, key.Clone(), largestPointKey) + } + // Store the max of (exciseSpan.End, rdel.Start) in firstRangeDel. This + // needs to be a copy if the key is owned by the range del iter. + var firstRangeDel []byte + rdel := rangeDelIter.SeekGE(exciseSpan.End) + if rdel != nil { + firstRangeDel = append(firstRangeDel[:0], rdel.Start...) + if d.cmp(firstRangeDel, exciseSpan.End) < 0 { + firstRangeDel = exciseSpan.End + } + } + if firstRangeDel != nil { + smallestPointKey := rdel.SmallestKey() + smallestPointKey.UserKey = firstRangeDel + rightFile.ExtendPointKeyBounds(d.cmp, smallestPointKey, largestPointKey) + } + } + if m.HasRangeKeys && !exciseSpan.Contains(d.cmp, m.LargestRangeKey) { + // This file will contain range keys. + largestRangeKey := m.LargestRangeKey + if rangeKeyIter == nil { + var err error + rangeKeyIter, err = d.tableNewRangeKeyIter(m, keyspan.SpanIterOptions{}) + if err != nil { + return nil, err + } + defer rangeKeyIter.Close() + } + // Store the max of (exciseSpan.End, rkey.Start) in firstRangeKey. This + // needs to be a copy if the key is owned by the range key iter. + var firstRangeKey []byte + rkey := rangeKeyIter.SeekGE(exciseSpan.End) + if rkey != nil { + firstRangeKey = append(firstRangeKey[:0], rkey.Start...) + if d.cmp(firstRangeKey, exciseSpan.End) < 0 { + firstRangeKey = exciseSpan.End + } + } + if firstRangeKey != nil { + smallestRangeKey := rkey.SmallestKey() + smallestRangeKey.UserKey = firstRangeKey + // We call ExtendRangeKeyBounds so any internal boundType fields are + // set correctly. Note that this is mildly wasteful as we'll be comparing + // rightFile.{Smallest,Largest}RangeKey with themselves, which can be + // avoided if we exported ExtendOverallKeyBounds or so. + rightFile.ExtendRangeKeyBounds(d.cmp, smallestRangeKey, largestRangeKey) + } + } + if rightFile.HasRangeKeys || rightFile.HasPointKeys { + var err error + rightFile.Size, err = d.tableCache.estimateSize(m, rightFile.Smallest.UserKey, rightFile.Largest.UserKey) + if err != nil { + return nil, err + } + if rightFile.Size == 0 { + // On occasion, estimateSize gives us a low estimate, i.e. a 0 file size, + // such as if the excised file only has range keys/dels and no point keys. + // This can cause panics in places where we divide by file sizes. Correct + // for it here. + rightFile.Size = 1 + } + rightFile.ValidateVirtual(m) + d.checkVirtualBounds(rightFile) + ve.NewFiles = append(ve.NewFiles, newFileEntry{Level: level, Meta: rightFile}) + needsBacking = true + numCreatedFiles++ + } + + if needsBacking && !m.Virtual { + // If m is virtual, then its file backing is already known to the manifest. + // We don't need to create another file backing. Note that there must be + // only one CreatedBackingTables entry per backing sstable. This is + // indicated by the VersionEdit.CreatedBackingTables invariant. + ve.CreatedBackingTables = append(ve.CreatedBackingTables, m.FileBacking) + } + + if err := rightFile.Validate(d.cmp, d.opts.Comparer.FormatKey); err != nil { + return nil, err + } + return ve.NewFiles[len(ve.NewFiles)-numCreatedFiles:], nil +} + type ingestTargetLevelFunc func( newIters tableNewIters, newRangeKeyIter keyspan.TableNewSpanIter, iterOps IterOptions, - cmp Compare, + comparer *Comparer, v *version, baseLevel int, compactions map[*compaction]struct{}, meta *fileMetadata, -) (int, error) + suggestSplit bool, +) (int, *fileMetadata, error) + +type ingestSplitFile struct { + // ingestFile is the file being ingested. + ingestFile *fileMetadata + // splitFile is the file that needs to be split to allow ingestFile to slot + // into `level` level. + splitFile *fileMetadata + // The level where ingestFile will go (and where splitFile already is). + level int +} + +// ingestSplit splits files specified in `files` and updates ve in-place to +// account for existing files getting split into two virtual sstables. The map +// `replacedFiles` contains an in-progress map of all files that have been +// replaced with new virtual sstables in this version edit so far, which is also +// updated in-place. +// +// d.mu as well as the manifest lock must be held when calling this method. +func (d *DB) ingestSplit( + ve *versionEdit, + updateMetrics func(*fileMetadata, int, []newFileEntry), + files []ingestSplitFile, + replacedFiles map[base.FileNum][]newFileEntry, +) error { + for _, s := range files { + // replacedFiles can be thought of as a tree, where we start iterating with + // s.splitFile and run its fileNum through replacedFiles, then find which of + // the replaced files overlaps with s.ingestFile, which becomes the new + // splitFile, then we check splitFile's replacements in replacedFiles again + // for overlap with s.ingestFile, and so on until we either can't find the + // current splitFile in replacedFiles (i.e. that's the file that now needs to + // be split), or we don't find a file that overlaps with s.ingestFile, which + // means a prior ingest split already produced enough room for s.ingestFile + // to go into this level without necessitating another ingest split. + splitFile := s.splitFile + for splitFile != nil { + replaced, ok := replacedFiles[splitFile.FileNum] + if !ok { + break + } + updatedSplitFile := false + for i := range replaced { + if replaced[i].Meta.Overlaps(d.cmp, s.ingestFile.Smallest.UserKey, s.ingestFile.Largest.UserKey, s.ingestFile.Largest.IsExclusiveSentinel()) { + if updatedSplitFile { + // This should never happen because the earlier ingestTargetLevel + // function only finds split file candidates that are guaranteed to + // have no data overlap, only boundary overlap. See the comments + // in that method to see the definitions of data vs boundary + // overlap. That, plus the fact that files in `replaced` are + // guaranteed to have file bounds that are tight on user keys + // (as that's what `d.excise` produces), means that the only case + // where we overlap with two or more files in `replaced` is if we + // actually had data overlap all along, or if the ingestion files + // were overlapping, either of which is an invariant violation. + panic("updated with two files in ingestSplit") + } + splitFile = replaced[i].Meta + updatedSplitFile = true + } + } + if !updatedSplitFile { + // None of the replaced files overlapped with the file being ingested. + // This can happen if we've already excised a span overlapping with + // this file, or if we have consecutive ingested files that can slide + // within the same gap between keys in an existing file. For instance, + // if an existing file has keys a and g and we're ingesting b-c, d-e, + // the first loop iteration will split the existing file into one that + // ends in a and another that starts at g, and the second iteration will + // fall into this case and require no splitting. + // + // No splitting necessary. + splitFile = nil + } + } + if splitFile == nil { + continue + } + // NB: excise operates on [start, end). We're splitting at [start, end] + // (assuming !s.ingestFile.Largest.IsExclusiveSentinel()). The conflation + // of exclusive vs inclusive end bounds should not make a difference here + // as we're guaranteed to not have any data overlap between splitFile and + // s.ingestFile, so panic if we do see a newly added file with an endKey + // equalling s.ingestFile.Largest, and !s.ingestFile.Largest.IsExclusiveSentinel() + added, err := d.excise(KeyRange{Start: s.ingestFile.Smallest.UserKey, End: s.ingestFile.Largest.UserKey}, splitFile, ve, s.level) + if err != nil { + return err + } + if _, ok := ve.DeletedFiles[deletedFileEntry{ + Level: s.level, + FileNum: splitFile.FileNum, + }]; !ok { + panic("did not split file that was expected to be split") + } + replacedFiles[splitFile.FileNum] = added + for i := range added { + if s.ingestFile.Overlaps(d.cmp, added[i].Meta.Smallest.UserKey, added[i].Meta.Largest.UserKey, added[i].Meta.Largest.IsExclusiveSentinel()) { + panic("ingest-time split produced a file that overlaps with ingested file") + } + } + updateMetrics(splitFile, s.level, added) + } + // Flatten the version edit by removing any entries from ve.NewFiles that + // are also in ve.DeletedFiles. + newNewFiles := ve.NewFiles[:0] + for i := range ve.NewFiles { + fn := ve.NewFiles[i].Meta.FileNum + deEntry := deletedFileEntry{Level: ve.NewFiles[i].Level, FileNum: fn} + if _, ok := ve.DeletedFiles[deEntry]; ok { + delete(ve.DeletedFiles, deEntry) + } else { + newNewFiles = append(newNewFiles, ve.NewFiles[i]) + } + } + ve.NewFiles = newNewFiles + return nil +} func (d *DB) ingestApply( - jobID int, meta []*fileMetadata, findTargetLevel ingestTargetLevelFunc, mut *memTable, + jobID int, + lr ingestLoadResult, + findTargetLevel ingestTargetLevelFunc, + mut *memTable, + exciseSpan KeyRange, ) (*versionEdit, error) { d.mu.Lock() defer d.mu.Unlock() ve := &versionEdit{ - NewFiles: make([]newFileEntry, len(meta)), + NewFiles: make([]newFileEntry, lr.fileCount), + } + if exciseSpan.Valid() || (d.opts.Experimental.IngestSplit != nil && d.opts.Experimental.IngestSplit()) { + ve.DeletedFiles = map[manifest.DeletedFileEntry]*manifest.FileMetadata{} } metrics := make(map[int]*LevelMetrics) @@ -1076,26 +2010,101 @@ func (d *DB) ingestApply( // returns must unlock the manifest. d.mu.versions.logLock() - scheduleFlush := false if mut != nil { // Unref the mutable memtable to allows its flush to proceed. Now that we've // acquired the manifest lock, we can be certain that if the mutable // memtable has received more recent conflicting writes, the flush won't // beat us to applying to the manifest resulting in sequence number - // inversion. - scheduleFlush = mut.writerUnref() + // inversion. Even though we call maybeScheduleFlush right now, this flush + // will apply after our ingestion. + if mut.writerUnref() { + d.maybeScheduleFlush() + } } + shouldIngestSplit := d.opts.Experimental.IngestSplit != nil && + d.opts.Experimental.IngestSplit() && d.FormatMajorVersion() >= FormatVirtualSSTables current := d.mu.versions.currentVersion() baseLevel := d.mu.versions.picker.getBaseLevel() iterOps := IterOptions{logger: d.opts.Logger} - for i := range meta { + // filesToSplit is a list where each element is a pair consisting of a file + // being ingested and a file being split to make room for an ingestion into + // that level. Each ingested file will appear at most once in this list. It + // is possible for split files to appear twice in this list. + filesToSplit := make([]ingestSplitFile, 0) + checkCompactions := false + for i := 0; i < lr.fileCount; i++ { // Determine the lowest level in the LSM for which the sstable doesn't // overlap any existing files in the level. - m := meta[i] + var m *fileMetadata + sharedIdx := -1 + sharedLevel := -1 + externalFile := false + if i < len(lr.localMeta) { + // local file. + m = lr.localMeta[i] + } else if (i - len(lr.localMeta)) < len(lr.sharedMeta) { + // shared file. + sharedIdx = i - len(lr.localMeta) + m = lr.sharedMeta[sharedIdx] + sharedLevel = int(lr.sharedLevels[sharedIdx]) + } else { + // external file. + externalFile = true + m = lr.externalMeta[i-(len(lr.localMeta)+len(lr.sharedMeta))] + } f := &ve.NewFiles[i] var err error - f.Level, err = findTargetLevel(d.newIters, d.tableNewRangeKeyIter, iterOps, d.cmp, current, baseLevel, d.mu.compact.inProgress, m) + if sharedIdx >= 0 { + f.Level = sharedLevel + if f.Level < sharedLevelsStart { + panic("cannot slot a shared file higher than the highest shared level") + } + ve.CreatedBackingTables = append(ve.CreatedBackingTables, m.FileBacking) + } else { + if externalFile { + ve.CreatedBackingTables = append(ve.CreatedBackingTables, m.FileBacking) + } + var splitFile *fileMetadata + if exciseSpan.Valid() && exciseSpan.Contains(d.cmp, m.Smallest) && exciseSpan.Contains(d.cmp, m.Largest) { + // This file fits perfectly within the excise span. We can slot it at + // L6, or sharedLevelsStart - 1 if we have shared files. + if len(lr.sharedMeta) > 0 { + f.Level = sharedLevelsStart - 1 + if baseLevel > f.Level { + f.Level = 0 + } + } else { + f.Level = 6 + } + } else { + // TODO(bilal): findTargetLevel does disk IO (reading files for data + // overlap) even though we're holding onto d.mu. Consider unlocking + // d.mu while we do this. We already hold versions.logLock so we should + // not see any version applications while we're at this. The one + // complication here would be pulling out the mu.compact.inProgress + // check from findTargetLevel, as that requires d.mu to be held. + f.Level, splitFile, err = findTargetLevel( + d.newIters, d.tableNewRangeKeyIter, iterOps, d.opts.Comparer, current, baseLevel, d.mu.compact.inProgress, m, shouldIngestSplit) + } + + if splitFile != nil { + if invariants.Enabled { + if lf := current.Levels[f.Level].Find(d.cmp, splitFile); lf == nil { + panic("splitFile returned is not in level it should be") + } + } + // We take advantage of the fact that we won't drop the db mutex + // between now and the call to logAndApply. So, no files should + // get added to a new in-progress compaction at this point. We can + // avoid having to iterate on in-progress compactions to cancel them + // if none of the files being split have a compacting state. + if splitFile.IsCompacting() { + checkCompactions = true + } + filesToSplit = append(filesToSplit, ingestSplitFile{ingestFile: m, splitFile: splitFile, level: f.Level}) + } + } if err != nil { d.mu.versions.logUnlock() return nil, err @@ -1111,26 +2120,159 @@ func (d *DB) ingestApply( levelMetrics.BytesIngested += m.Size levelMetrics.TablesIngested++ } + // replacedFiles maps files excised due to exciseSpan (or splitFiles returned + // by ingestTargetLevel), to files that were created to replace it. This map + // is used to resolve references to split files in filesToSplit, as it is + // possible for a file that we want to split to no longer exist or have a + // newer fileMetadata due to a split induced by another ingestion file, or an + // excise. + replacedFiles := make(map[base.FileNum][]newFileEntry) + updateLevelMetricsOnExcise := func(m *fileMetadata, level int, added []newFileEntry) { + levelMetrics := metrics[level] + if levelMetrics == nil { + levelMetrics = &LevelMetrics{} + metrics[level] = levelMetrics + } + levelMetrics.NumFiles-- + levelMetrics.Size -= int64(m.Size) + for i := range added { + levelMetrics.NumFiles++ + levelMetrics.Size += int64(added[i].Meta.Size) + } + } + if exciseSpan.Valid() { + // Iterate through all levels and find files that intersect with exciseSpan. + // + // TODO(bilal): We could drop the DB mutex here as we don't need it for + // excises; we only need to hold the version lock which we already are + // holding. However releasing the DB mutex could mess with the + // ingestTargetLevel calculation that happened above, as it assumed that it + // had a complete view of in-progress compactions that wouldn't change + // until logAndApply is called. If we were to drop the mutex now, we could + // schedule another in-progress compaction that would go into the chosen target + // level and lead to file overlap within level (which would panic in + // logAndApply). We should drop the db mutex here, do the excise, then + // re-grab the DB mutex and rerun just the in-progress compaction check to + // see if any new compactions are conflicting with our chosen target levels + // for files, and if they are, we should signal those compactions to error + // out. + for level := range current.Levels { + overlaps := current.Overlaps(level, d.cmp, exciseSpan.Start, exciseSpan.End, true /* exclusiveEnd */) + iter := overlaps.Iter() + + for m := iter.First(); m != nil; m = iter.Next() { + newFiles, err := d.excise(exciseSpan, m, ve, level) + if err != nil { + return nil, err + } + + if _, ok := ve.DeletedFiles[deletedFileEntry{ + Level: level, + FileNum: m.FileNum, + }]; !ok { + // We did not excise this file. + continue + } + replacedFiles[m.FileNum] = newFiles + updateLevelMetricsOnExcise(m, level, newFiles) + } + } + } + if len(filesToSplit) > 0 { + // For the same reasons as the above call to excise, we hold the db mutex + // while calling this method. + if err := d.ingestSplit(ve, updateLevelMetricsOnExcise, filesToSplit, replacedFiles); err != nil { + return nil, err + } + } + if len(filesToSplit) > 0 || exciseSpan.Valid() { + for c := range d.mu.compact.inProgress { + if c.versionEditApplied { + continue + } + // Check if this compaction overlaps with the excise span. Note that just + // checking if the inputs individually overlap with the excise span + // isn't sufficient; for instance, a compaction could have [a,b] and [e,f] + // as inputs and write it all out as [a,b,e,f] in one sstable. If we're + // doing a [c,d) excise at the same time as this compaction, we will have + // to error out the whole compaction as we can't guarantee it hasn't/won't + // write a file overlapping with the excise span. + if exciseSpan.OverlapsInternalKeyRange(d.cmp, c.smallest, c.largest) { + c.cancel.Store(true) + } + // Check if this compaction's inputs have been replaced due to an + // ingest-time split. In that case, cancel the compaction as a newly picked + // compaction would need to include any new files that slid in between + // previously-existing files. Note that we cancel any compaction that has a + // file that was ingest-split as an input, even if it started before this + // ingestion. + if checkCompactions { + for i := range c.inputs { + iter := c.inputs[i].files.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if _, ok := replacedFiles[f.FileNum]; ok { + c.cancel.Store(true) + break + } + } + } + } + } + // Check for any EventuallyFileOnlySnapshots that could be watching for + // an excise on this span. + if exciseSpan.Valid() { + for s := d.mu.snapshots.root.next; s != &d.mu.snapshots.root; s = s.next { + if s.efos == nil { + continue + } + efos := s.efos + // TODO(bilal): We can make this faster by taking advantage of the sorted + // nature of protectedRanges to do a sort.Search, or even maintaining a + // global list of all protected ranges instead of having to peer into every + // snapshot. + for i := range efos.protectedRanges { + if efos.protectedRanges[i].OverlapsKeyRange(d.cmp, exciseSpan) { + efos.excised.Store(true) + break + } + } + } + } + } if err := d.mu.versions.logAndApply(jobID, ve, metrics, false /* forceRotation */, func() []compactionInfo { return d.getInProgressCompactionInfoLocked(nil) }); err != nil { return nil, err } + + d.mu.versions.metrics.Ingest.Count++ + d.updateReadStateLocked(d.opts.DebugCheck) + // updateReadStateLocked could have generated obsolete tables, schedule a + // cleanup job if necessary. + d.deleteObsoleteFiles(jobID) d.updateTableStatsLocked(ve.NewFiles) - d.deleteObsoleteFiles(jobID, false /* waitForOngoing */) // The ingestion may have pushed a level over the threshold for compaction, // so check to see if one is necessary and schedule it. d.maybeScheduleCompaction() - if scheduleFlush { - d.maybeScheduleFlush() + var toValidate []manifest.NewFileEntry + dedup := make(map[base.DiskFileNum]struct{}) + for _, entry := range ve.NewFiles { + if _, ok := dedup[entry.Meta.FileBacking.DiskFileNum]; !ok { + toValidate = append(toValidate, entry) + dedup[entry.Meta.FileBacking.DiskFileNum] = struct{}{} + } } - d.maybeValidateSSTablesLocked(ve.NewFiles) + d.maybeValidateSSTablesLocked(toValidate) return ve, nil } // maybeValidateSSTablesLocked adds the slice of newFileEntrys to the pending // queue of files to be validated, when the feature is enabled. +// +// Note that if two entries with the same backing file are added twice, then the +// block checksums for the backing file will be validated twice. +// // DB.mu must be locked when calling. func (d *DB) maybeValidateSSTablesLocked(newFiles []newFileEntry) { // Only add to the validation queue when the feature is enabled. @@ -1197,9 +2339,19 @@ func (d *DB) validateSSTables() { } } - err := d.tableCache.withReader(f.Meta, func(r *sstable.Reader) error { - return r.ValidateBlockChecksums() - }) + var err error + if f.Meta.Virtual { + err = d.tableCache.withVirtualReader( + f.Meta.VirtualMeta(), func(v sstable.VirtualReader) error { + return v.ValidateBlockChecksumsOnBacking() + }) + } else { + err = d.tableCache.withReader( + f.Meta.PhysicalMeta(), func(r *sstable.Reader) error { + return r.ValidateBlockChecksums() + }) + } + if err != nil { // TODO(travers): Hook into the corruption reporting pipeline, once // available. See pebble#1192. diff --git a/vendor/github.com/cockroachdb/pebble/internal.go b/vendor/github.com/cockroachdb/pebble/internal.go index 99fec08..61a4284 100644 --- a/vendor/github.com/cockroachdb/pebble/internal.go +++ b/vendor/github.com/cockroachdb/pebble/internal.go @@ -22,7 +22,8 @@ const ( InternalKeyKindRangeKeySet = base.InternalKeyKindRangeKeySet InternalKeyKindRangeKeyUnset = base.InternalKeyKindRangeKeyUnset InternalKeyKindRangeKeyDelete = base.InternalKeyKindRangeKeyDelete - InternalKeyKindIngestSST = base.InternalKeyIngestSST + InternalKeyKindIngestSST = base.InternalKeyKindIngestSST + InternalKeyKindDeleteSized = base.InternalKeyKindDeleteSized InternalKeyKindInvalid = base.InternalKeyKindInvalid InternalKeySeqNumBatch = base.InternalKeySeqNumBatch InternalKeySeqNumMax = base.InternalKeySeqNumMax diff --git a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/arena.go b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/arena.go index ad0783d..011c3b0 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/arena.go +++ b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/arena.go @@ -18,22 +18,21 @@ package arenaskl import ( - "math" "sync/atomic" "unsafe" "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/constants" + "github.com/cockroachdb/pebble/internal/invariants" ) // Arena is lock-free. type Arena struct { - n uint64 + n atomic.Uint64 buf []byte } -const ( - align4 = 3 -) +const nodeAlignment = 4 var ( // ErrArenaFull indicates that the arena is full and cannot perform any more @@ -44,20 +43,28 @@ var ( // NewArena allocates a new arena using the specified buffer as the backing // store. func NewArena(buf []byte) *Arena { - // Don't store data at position 0 in order to reserve offset=0 as a kind - // of nil pointer. - return &Arena{ - n: 1, + if len(buf) > constants.MaxUint32OrInt { + if invariants.Enabled { + panic(errors.AssertionFailedf("attempting to create arena of size %d", len(buf))) + } + buf = buf[:constants.MaxUint32OrInt] + } + a := &Arena{ buf: buf, } + // We don't store data at position 0 in order to reserve offset=0 as a kind of + // nil pointer. + a.n.Store(1) + return a } // Size returns the number of bytes allocated by the arena. func (a *Arena) Size() uint32 { - s := atomic.LoadUint64(&a.n) - if s > math.MaxUint32 { - // Saturate at MaxUint32. - return math.MaxUint32 + s := a.n.Load() + if s > constants.MaxUint32OrInt { + // The last failed allocation can push the size higher than len(a.buf). + // Saturate at the maximum representable offset. + return constants.MaxUint32OrInt } return uint32(s) } @@ -67,24 +74,33 @@ func (a *Arena) Capacity() uint32 { return uint32(len(a.buf)) } -func (a *Arena) alloc(size, align, overflow uint32) (uint32, uint32, error) { +// alloc allocates a buffer of the given size and with the given alignment +// (which must be a power of 2). +// +// If overflow is not 0, it also ensures that many bytes after the buffer are +// inside the arena (this is used for structures that are larger than the +// requested size but don't use those extra bytes). +func (a *Arena) alloc(size, alignment, overflow uint32) (uint32, uint32, error) { + if invariants.Enabled && (alignment&(alignment-1)) != 0 { + panic(errors.AssertionFailedf("invalid alignment %d", alignment)) + } // Verify that the arena isn't already full. - origSize := atomic.LoadUint64(&a.n) + origSize := a.n.Load() if int(origSize) > len(a.buf) { return 0, 0, ErrArenaFull } // Pad the allocation with enough bytes to ensure the requested alignment. - padded := uint32(size) + align + padded := uint64(size) + uint64(alignment) - 1 - newSize := atomic.AddUint64(&a.n, uint64(padded)) - if int(newSize)+int(overflow) > len(a.buf) { + newSize := a.n.Add(padded) + if newSize+uint64(overflow) > uint64(len(a.buf)) { return 0, 0, ErrArenaFull } // Return the aligned offset. - offset := (uint32(newSize) - padded + align) & ^align - return offset, padded, nil + offset := (uint32(newSize) - size) & ^(alignment - 1) + return offset, uint32(padded), nil } func (a *Arena) getBytes(offset uint32, size uint32) []byte { diff --git a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/iterator.go b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/iterator.go index 3a40a20..bad4909 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/iterator.go +++ b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/iterator.go @@ -18,7 +18,6 @@ package arenaskl import ( - "encoding/binary" "sync" "github.com/cockroachdb/pebble/internal/base" @@ -240,17 +239,8 @@ func (it *Iterator) SetBounds(lower, upper []byte) { } func (it *Iterator) decodeKey() { - b := it.list.arena.getBytes(it.nd.keyOffset, it.nd.keySize) - // This is a manual inline of base.DecodeInternalKey, because the Go compiler - // seems to refuse to automatically inline it currently. - l := len(b) - 8 - if l >= 0 { - it.key.Trailer = binary.LittleEndian.Uint64(b[l:]) - it.key.UserKey = b[:l:l] - } else { - it.key.Trailer = uint64(base.InternalKeyKindInvalid) - it.key.UserKey = nil - } + it.key.UserKey = it.list.arena.getBytes(it.nd.keyOffset, it.nd.keySize) + it.key.Trailer = it.nd.keyTrailer } func (it *Iterator) seekForBaseSplice(key []byte) (prev, next *node, found bool) { diff --git a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/node.go b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/node.go index a605d9d..d464bc5 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/node.go +++ b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/node.go @@ -29,25 +29,27 @@ import ( // is used here. If a key/value overflows a uint32, it should not be added to // the skiplist. func MaxNodeSize(keySize, valueSize uint32) uint64 { - return uint64(maxNodeSize) + uint64(keySize) + uint64(valueSize) + align4 + const maxPadding = nodeAlignment - 1 + return uint64(maxNodeSize) + uint64(keySize) + uint64(valueSize) + maxPadding } type links struct { - nextOffset uint32 - prevOffset uint32 + nextOffset atomic.Uint32 + prevOffset atomic.Uint32 } func (l *links) init(prevOffset, nextOffset uint32) { - l.nextOffset = nextOffset - l.prevOffset = prevOffset + l.nextOffset.Store(nextOffset) + l.prevOffset.Store(prevOffset) } type node struct { // Immutable fields, so no need to lock to access key. - keyOffset uint32 - keySize uint32 - valueSize uint32 - allocSize uint32 + keyOffset uint32 + keySize uint32 + keyTrailer uint64 + valueSize uint32 + allocSize uint32 // Most nodes do not need to use the full height of the tower, since the // probability of each successive level decreases exponentially. Because @@ -65,7 +67,7 @@ func newNode( if height < 1 || height > maxHeight { panic("height cannot be less than one or greater than the max height") } - keySize := key.Size() + keySize := len(key.UserKey) if int64(keySize) > math.MaxUint32 { panic("key is too large") } @@ -81,8 +83,8 @@ func newNode( if err != nil { return } - - key.Encode(nd.getKeyBytes(arena)) + nd.keyTrailer = key.Trailer + copy(nd.getKeyBytes(arena), key.UserKey) copy(nd.getValue(arena), value) return } @@ -93,7 +95,7 @@ func newRawNode(arena *Arena, height uint32, keySize, valueSize uint32) (nd *nod unusedSize := uint32((maxHeight - int(height)) * linksSize) nodeSize := uint32(maxNodeSize) - unusedSize - nodeOffset, allocSize, err := arena.alloc(nodeSize+keySize+valueSize, align4, unusedSize) + nodeOffset, allocSize, err := arena.alloc(nodeSize+keySize+valueSize, nodeAlignment, unusedSize) if err != nil { return } @@ -115,17 +117,17 @@ func (n *node) getValue(arena *Arena) []byte { } func (n *node) nextOffset(h int) uint32 { - return atomic.LoadUint32(&n.tower[h].nextOffset) + return n.tower[h].nextOffset.Load() } func (n *node) prevOffset(h int) uint32 { - return atomic.LoadUint32(&n.tower[h].prevOffset) + return n.tower[h].prevOffset.Load() } func (n *node) casNextOffset(h int, old, val uint32) bool { - return atomic.CompareAndSwapUint32(&n.tower[h].nextOffset, old, val) + return n.tower[h].nextOffset.CompareAndSwap(old, val) } func (n *node) casPrevOffset(h int, old, val uint32) bool { - return atomic.CompareAndSwapUint32(&n.tower[h].prevOffset, old, val) + return n.tower[h].prevOffset.CompareAndSwap(old, val) } diff --git a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/skl.go b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/skl.go index 3f29447..ef1ebfc 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/arenaskl/skl.go +++ b/vendor/github.com/cockroachdb/pebble/internal/arenaskl/skl.go @@ -44,7 +44,6 @@ Key differences: package arenaskl // import "github.com/cockroachdb/pebble/internal/arenaskl" import ( - "encoding/binary" "math" "runtime" "sync/atomic" @@ -68,7 +67,7 @@ const ( // keys. var ErrRecordExists = errors.New("record with this key already exists") -// Skiplist is a fast, cocnurrent skiplist implementation that supports forward +// Skiplist is a fast, concurrent skiplist implementation that supports forward // and backward iteration. See batchskl.Skiplist for a non-concurrent // skiplist. Keys and values are immutable once added to the skiplist and // deletion is not supported. Instead, higher-level code is expected to add new @@ -80,7 +79,7 @@ type Skiplist struct { cmp base.Compare head *node tail *node - height uint32 // Current height. 1 <= height <= maxHeight. CAS. + height atomic.Uint32 // Current height. 1 <= height <= maxHeight. CAS. // If set to true by tests, then extra delays are added to make it easier to // detect unusual race conditions. @@ -140,22 +139,22 @@ func (s *Skiplist) Reset(arena *Arena, cmp base.Compare) { headOffset := arena.getPointerOffset(unsafe.Pointer(head)) tailOffset := arena.getPointerOffset(unsafe.Pointer(tail)) for i := 0; i < maxHeight; i++ { - head.tower[i].nextOffset = tailOffset - tail.tower[i].prevOffset = headOffset + head.tower[i].nextOffset.Store(tailOffset) + tail.tower[i].prevOffset.Store(headOffset) } *s = Skiplist{ - arena: arena, - cmp: cmp, - head: head, - tail: tail, - height: 1, + arena: arena, + cmp: cmp, + head: head, + tail: tail, } + s.height.Store(1) } // Height returns the height of the highest tower within any of the nodes that // have ever been allocated as part of this skiplist. -func (s *Skiplist) Height() uint32 { return atomic.LoadUint32(&s.height) } +func (s *Skiplist) Height() uint32 { return s.height.Load() } // Arena returns the arena backing this skiplist. func (s *Skiplist) Arena() *Arena { return s.arena } @@ -327,7 +326,7 @@ func (s *Skiplist) newNode( // Try to increase s.height via CAS. listHeight := s.Height() for height > listHeight { - if atomic.CompareAndSwapUint32(&s.height, listHeight, height) { + if s.height.CompareAndSwap(listHeight, height) { // Successfully increased skiplist.height. break } @@ -412,26 +411,19 @@ func (s *Skiplist) findSpliceForLevel( offset, size := next.keyOffset, next.keySize nextKey := s.arena.buf[offset : offset+size] - n := int32(size) - 8 - cmp := s.cmp(key.UserKey, nextKey[:n]) + cmp := s.cmp(key.UserKey, nextKey) if cmp < 0 { // We are done for this level, since prev.key < key < next.key. break } if cmp == 0 { // User-key equality. - var nextTrailer uint64 - if n >= 0 { - nextTrailer = binary.LittleEndian.Uint64(nextKey[n:]) - } else { - nextTrailer = uint64(base.InternalKeyKindInvalid) - } - if key.Trailer == nextTrailer { + if key.Trailer == next.keyTrailer { // Internal key equality. found = true break } - if key.Trailer > nextTrailer { + if key.Trailer > next.keyTrailer { // We are done for this level, since prev.key < key < next.key. break } @@ -446,8 +438,7 @@ func (s *Skiplist) findSpliceForLevel( func (s *Skiplist) keyIsAfterNode(nd *node, key base.InternalKey) bool { ndKey := s.arena.buf[nd.keyOffset : nd.keyOffset+nd.keySize] - n := int32(nd.keySize) - 8 - cmp := s.cmp(ndKey[:n], key.UserKey) + cmp := s.cmp(ndKey, key.UserKey) if cmp < 0 { return true } @@ -455,25 +446,19 @@ func (s *Skiplist) keyIsAfterNode(nd *node, key base.InternalKey) bool { return false } // User-key equality. - var ndTrailer uint64 - if n >= 0 { - ndTrailer = binary.LittleEndian.Uint64(ndKey[n:]) - } else { - ndTrailer = uint64(base.InternalKeyKindInvalid) - } - if key.Trailer == ndTrailer { + if key.Trailer == nd.keyTrailer { // Internal key equality. return false } - return key.Trailer < ndTrailer + return key.Trailer < nd.keyTrailer } func (s *Skiplist) getNext(nd *node, h int) *node { - offset := atomic.LoadUint32(&nd.tower[h].nextOffset) + offset := nd.tower[h].nextOffset.Load() return (*node)(s.arena.getPointer(offset)) } func (s *Skiplist) getPrev(nd *node, h int) *node { - offset := atomic.LoadUint32(&nd.tower[h].prevOffset) + offset := nd.tower[h].prevOffset.Load() return (*node)(s.arena.getPointer(offset)) } diff --git a/vendor/github.com/cockroachdb/pebble/internal/base/filenames.go b/vendor/github.com/cockroachdb/pebble/internal/base/filenames.go index 269d14b..d2ba2f0 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/base/filenames.go +++ b/vendor/github.com/cockroachdb/pebble/internal/base/filenames.go @@ -20,6 +20,41 @@ type FileNum uint64 // String returns a string representation of the file number. func (fn FileNum) String() string { return fmt.Sprintf("%06d", fn) } +// SafeFormat implements redact.SafeFormatter. +func (fn FileNum) SafeFormat(w redact.SafePrinter, _ rune) { + w.Printf("%06d", redact.SafeUint(fn)) +} + +// DiskFileNum converts a FileNum to a DiskFileNum. DiskFileNum should only be +// called if the caller can ensure that the FileNum belongs to a physical file +// on disk. These could be manifests, log files, physical sstables on disk, the +// options file, but not virtual sstables. +func (fn FileNum) DiskFileNum() DiskFileNum { + return DiskFileNum{fn} +} + +// A DiskFileNum is just a FileNum belonging to a file which exists on disk. +// Note that a FileNum is an internal DB identifier and it could belong to files +// which don't exist on disk. An example would be virtual sstable FileNums. +// Converting a DiskFileNum to a FileNum is always valid, whereas converting a +// FileNum to DiskFileNum may not be valid and care should be taken to prove +// that the FileNum actually exists on disk. +type DiskFileNum struct { + fn FileNum +} + +func (dfn DiskFileNum) String() string { return dfn.fn.String() } + +// SafeFormat implements redact.SafeFormatter. +func (dfn DiskFileNum) SafeFormat(w redact.SafePrinter, verb rune) { + dfn.fn.SafeFormat(w, verb) +} + +// FileNum converts a DiskFileNum to a FileNum. This conversion is always valid. +func (dfn DiskFileNum) FileNum() FileNum { + return dfn.fn +} + // FileType enumerates the types of files found in a DB. type FileType int @@ -36,92 +71,92 @@ const ( ) // MakeFilename builds a filename from components. -func MakeFilename(fileType FileType, fileNum FileNum) string { +func MakeFilename(fileType FileType, dfn DiskFileNum) string { switch fileType { case FileTypeLog: - return fmt.Sprintf("%s.log", fileNum) + return fmt.Sprintf("%s.log", dfn) case FileTypeLock: return "LOCK" case FileTypeTable: - return fmt.Sprintf("%s.sst", fileNum) + return fmt.Sprintf("%s.sst", dfn) case FileTypeManifest: - return fmt.Sprintf("MANIFEST-%s", fileNum) + return fmt.Sprintf("MANIFEST-%s", dfn) case FileTypeCurrent: return "CURRENT" case FileTypeOptions: - return fmt.Sprintf("OPTIONS-%s", fileNum) + return fmt.Sprintf("OPTIONS-%s", dfn) case FileTypeOldTemp: - return fmt.Sprintf("CURRENT.%s.dbtmp", fileNum) + return fmt.Sprintf("CURRENT.%s.dbtmp", dfn) case FileTypeTemp: - return fmt.Sprintf("temporary.%s.dbtmp", fileNum) + return fmt.Sprintf("temporary.%s.dbtmp", dfn) } panic("unreachable") } // MakeFilepath builds a filepath from components. -func MakeFilepath(fs vfs.FS, dirname string, fileType FileType, fileNum FileNum) string { - return fs.PathJoin(dirname, MakeFilename(fileType, fileNum)) +func MakeFilepath(fs vfs.FS, dirname string, fileType FileType, dfn DiskFileNum) string { + return fs.PathJoin(dirname, MakeFilename(fileType, dfn)) } // ParseFilename parses the components from a filename. -func ParseFilename(fs vfs.FS, filename string) (fileType FileType, fileNum FileNum, ok bool) { +func ParseFilename(fs vfs.FS, filename string) (fileType FileType, dfn DiskFileNum, ok bool) { filename = fs.PathBase(filename) switch { case filename == "CURRENT": - return FileTypeCurrent, 0, true + return FileTypeCurrent, DiskFileNum{0}, true case filename == "LOCK": - return FileTypeLock, 0, true + return FileTypeLock, DiskFileNum{0}, true case strings.HasPrefix(filename, "MANIFEST-"): - fileNum, ok = parseFileNum(filename[len("MANIFEST-"):]) + dfn, ok = parseDiskFileNum(filename[len("MANIFEST-"):]) if !ok { break } - return FileTypeManifest, fileNum, true + return FileTypeManifest, dfn, true case strings.HasPrefix(filename, "OPTIONS-"): - fileNum, ok = parseFileNum(filename[len("OPTIONS-"):]) + dfn, ok = parseDiskFileNum(filename[len("OPTIONS-"):]) if !ok { break } - return FileTypeOptions, fileNum, ok + return FileTypeOptions, dfn, ok case strings.HasPrefix(filename, "CURRENT.") && strings.HasSuffix(filename, ".dbtmp"): s := strings.TrimSuffix(filename[len("CURRENT."):], ".dbtmp") - fileNum, ok = parseFileNum(s) + dfn, ok = parseDiskFileNum(s) if !ok { break } - return FileTypeOldTemp, fileNum, ok + return FileTypeOldTemp, dfn, ok case strings.HasPrefix(filename, "temporary.") && strings.HasSuffix(filename, ".dbtmp"): s := strings.TrimSuffix(filename[len("temporary."):], ".dbtmp") - fileNum, ok = parseFileNum(s) + dfn, ok = parseDiskFileNum(s) if !ok { break } - return FileTypeTemp, fileNum, ok + return FileTypeTemp, dfn, ok default: i := strings.IndexByte(filename, '.') if i < 0 { break } - fileNum, ok = parseFileNum(filename[:i]) + dfn, ok = parseDiskFileNum(filename[:i]) if !ok { break } switch filename[i+1:] { case "sst": - return FileTypeTable, fileNum, true + return FileTypeTable, dfn, true case "log": - return FileTypeLog, fileNum, true + return FileTypeLog, dfn, true } } - return 0, fileNum, false + return 0, dfn, false } -func parseFileNum(s string) (fileNum FileNum, ok bool) { +func parseDiskFileNum(s string) (dfn DiskFileNum, ok bool) { u, err := strconv.ParseUint(s, 10, 64) if err != nil { - return fileNum, false + return dfn, false } - return FileNum(u), true + return DiskFileNum{FileNum(u)}, true } // A Fataler fatals a process with a message when called. diff --git a/vendor/github.com/cockroachdb/pebble/internal/base/internal.go b/vendor/github.com/cockroachdb/pebble/internal/base/internal.go index 208b88f..259ef98 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/base/internal.go +++ b/vendor/github.com/cockroachdb/pebble/internal/base/internal.go @@ -9,6 +9,17 @@ import ( "fmt" "strconv" "strings" + + "github.com/cockroachdb/redact" +) + +const ( + // SeqNumZero is the zero sequence number, set by compactions if they can + // guarantee there are no keys underneath an internal key. + SeqNumZero = uint64(0) + // SeqNumStart is the first sequence number assigned to a key. Sequence + // numbers 1-9 are reserved for potential future use. + SeqNumStart = uint64(10) ) // InternalKeyKind enumerates the kind of key: a deletion tombstone, a set @@ -24,6 +35,14 @@ const ( //InternalKeyKindColumnFamilyDeletion InternalKeyKind = 4 //InternalKeyKindColumnFamilyValue InternalKeyKind = 5 //InternalKeyKindColumnFamilyMerge InternalKeyKind = 6 + + // InternalKeyKindSingleDelete (SINGLEDEL) is a performance optimization + // solely for compactions (to reduce write amp and space amp). Readers other + // than compactions should treat SINGLEDEL as equivalent to a DEL. + // Historically, it was simpler for readers other than compactions to treat + // SINGLEDEL as equivalent to DEL, but as of the introduction of + // InternalKeyKindSSTableInternalObsoleteBit, this is also necessary for + // correctness. InternalKeyKindSingleDelete InternalKeyKind = 7 //InternalKeyKindColumnFamilySingleDelete InternalKeyKind = 8 //InternalKeyKindBeginPrepareXID InternalKeyKind = 9 @@ -60,14 +79,21 @@ const ( InternalKeyKindRangeKeyUnset InternalKeyKind = 20 InternalKeyKindRangeKeySet InternalKeyKind = 21 - // InternalKeyIngestSST is used to distinguish a batch that corresponds to + // InternalKeyKindIngestSST is used to distinguish a batch that corresponds to // the WAL entry for ingested sstables that are added to the flushable // queue. This InternalKeyKind cannot appear, amongst other key kinds in a // batch, or in an sstable. - InternalKeyIngestSST InternalKeyKind = 22 + InternalKeyKindIngestSST InternalKeyKind = 22 - // This maximum value isn't part of the file format. It's unlikely, - // but future extensions may increase this value. + // InternalKeyKindDeleteSized keys behave identically to + // InternalKeyKindDelete keys, except that they hold an associated uint64 + // value indicating the (len(key)+len(value)) of the shadowed entry the + // tombstone is expected to delete. This value is used to inform compaction + // heuristics, but is not required to be accurate for correctness. + InternalKeyKindDeleteSized InternalKeyKind = 23 + + // This maximum value isn't part of the file format. Future extensions may + // increase this value. // // When constructing an internal key to pass to DB.Seek{GE,LE}, // internalKeyComparer sorts decreasing by kind (after sorting increasing by @@ -75,14 +101,19 @@ const ( // which sorts 'less than or equal to' any other valid internalKeyKind, when // searching for any kind of internal key formed by a certain user key and // seqNum. - InternalKeyKindMax InternalKeyKind = 22 + InternalKeyKindMax InternalKeyKind = 23 + + // Internal to the sstable format. Not exposed by any sstable iterator. + // Declared here to prevent definition of valid key kinds that set this bit. + InternalKeyKindSSTableInternalObsoleteBit InternalKeyKind = 64 + InternalKeyKindSSTableInternalObsoleteMask InternalKeyKind = 191 // InternalKeyZeroSeqnumMaxTrailer is the largest trailer with a // zero sequence number. - InternalKeyZeroSeqnumMaxTrailer = uint64(InternalKeyKindInvalid) + InternalKeyZeroSeqnumMaxTrailer = uint64(255) // A marker for an invalid key. - InternalKeyKindInvalid InternalKeyKind = 255 + InternalKeyKindInvalid InternalKeyKind = InternalKeyKindSSTableInternalObsoleteMask // InternalKeySeqNumBatch is a bit that is set on batch sequence numbers // which prevents those entries from being excluded from iteration. @@ -105,6 +136,9 @@ const ( InternalKeyBoundaryRangeKey = (InternalKeySeqNumMax << 8) | uint64(InternalKeyKindRangeKeySet) ) +// Assert InternalKeyKindSSTableInternalObsoleteBit > InternalKeyKindMax +const _ = uint(InternalKeyKindSSTableInternalObsoleteBit - InternalKeyKindMax - 1) + var internalKeyKindNames = []string{ InternalKeyKindDelete: "DEL", InternalKeyKindSet: "SET", @@ -117,7 +151,8 @@ var internalKeyKindNames = []string{ InternalKeyKindRangeKeySet: "RANGEKEYSET", InternalKeyKindRangeKeyUnset: "RANGEKEYUNSET", InternalKeyKindRangeKeyDelete: "RANGEKEYDEL", - InternalKeyIngestSST: "INGESTSST", + InternalKeyKindIngestSST: "INGESTSST", + InternalKeyKindDeleteSized: "DELSIZED", InternalKeyKindInvalid: "INVALID", } @@ -128,6 +163,11 @@ func (k InternalKeyKind) String() string { return fmt.Sprintf("UNKNOWN:%d", k) } +// SafeFormat implements redact.SafeFormatter. +func (k InternalKeyKind) SafeFormat(w redact.SafePrinter, _ rune) { + w.Print(redact.SafeString(k.String())) +} + // InternalKey is a key used for the in-memory and on-disk partial DBs that // make up a pebble DB. // @@ -194,6 +234,7 @@ var kindsMap = map[string]InternalKeyKind{ "DEL": InternalKeyKindDelete, "SINGLEDEL": InternalKeyKindSingleDelete, "RANGEDEL": InternalKeyKindRangeDelete, + "LOGDATA": InternalKeyKindLogData, "SET": InternalKeyKindSet, "MERGE": InternalKeyKindMerge, "INVALID": InternalKeyKindInvalid, @@ -202,6 +243,8 @@ var kindsMap = map[string]InternalKeyKind{ "RANGEKEYSET": InternalKeyKindRangeKeySet, "RANGEKEYUNSET": InternalKeyKindRangeKeyUnset, "RANGEKEYDEL": InternalKeyKindRangeKeyDelete, + "INGESTSST": InternalKeyKindIngestSST, + "DELSIZED": InternalKeyKindDeleteSized, } // ParseInternalKey parses the string representation of an internal key. The @@ -338,6 +381,11 @@ func (k InternalKey) SeqNum() uint64 { return k.Trailer >> 8 } +// SeqNumFromTrailer returns the sequence number component of a trailer. +func SeqNumFromTrailer(t uint64) uint64 { + return t >> 8 +} + // Visible returns true if the key is visible at the specified snapshot // sequence number. func (k InternalKey) Visible(snapshot, batchSnapshot uint64) bool { @@ -395,6 +443,13 @@ func (k InternalKey) Clone() InternalKey { } } +// CopyFrom converts this InternalKey into a clone of the passed-in InternalKey, +// reusing any space already used for the current UserKey. +func (k *InternalKey) CopyFrom(k2 InternalKey) { + k.UserKey = append(k.UserKey[:0], k2.UserKey...) + k.Trailer = k2.Trailer +} + // String returns a string representation of the key. func (k InternalKey) String() string { return fmt.Sprintf("%s#%d,%d", FormatBytes(k.UserKey), k.SeqNum(), k.Kind()) diff --git a/vendor/github.com/cockroachdb/pebble/internal/base/metrics.go b/vendor/github.com/cockroachdb/pebble/internal/base/metrics.go index 4a7789a..520edc3 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/base/metrics.go +++ b/vendor/github.com/cockroachdb/pebble/internal/base/metrics.go @@ -53,6 +53,15 @@ func (tm *ThroughputMetric) Rate() int64 { float64(time.Second)) } +// Utilization returns a percent [0, 1.0] indicating the percent of time +// work was performed. +func (tm *ThroughputMetric) Utilization() float64 { + if tm.WorkDuration == 0 { + return 0 + } + return float64(tm.WorkDuration) / float64(tm.WorkDuration+tm.IdleDuration) +} + // GaugeSampleMetric is used to measure a gauge value (e.g. queue length) by // accumulating samples of that gauge. type GaugeSampleMetric struct { diff --git a/vendor/github.com/cockroachdb/pebble/internal/batchskl/skl.go b/vendor/github.com/cockroachdb/pebble/internal/batchskl/skl.go index 73bfa0e..f56d95c 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/batchskl/skl.go +++ b/vendor/github.com/cockroachdb/pebble/internal/batchskl/skl.go @@ -63,14 +63,15 @@ import ( "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/constants" "golang.org/x/exp/rand" ) const ( maxHeight = 20 - maxNodeSize = int(unsafe.Sizeof(node{})) - linksSize = int(unsafe.Sizeof(links{})) - maxNodesSize = math.MaxUint32 + maxNodeSize = uint64(unsafe.Sizeof(node{})) + linksSize = uint64(unsafe.Sizeof(links{})) + maxNodesSize = constants.MaxUint32OrInt ) var ( @@ -281,7 +282,7 @@ func (s *Skiplist) newNode( panic("height cannot be less than one or greater than the max height") } - unusedSize := (maxHeight - int(height)) * linksSize + unusedSize := uint64(maxHeight-int(height)) * linksSize nodeOffset, err := s.alloc(uint32(maxNodeSize - unusedSize)) if err != nil { return 0, err @@ -296,13 +297,13 @@ func (s *Skiplist) newNode( } func (s *Skiplist) alloc(size uint32) (uint32, error) { - offset := len(s.nodes) + offset := uint64(len(s.nodes)) // We only have a need for memory up to offset + size, but we never want // to allocate a node whose tail points into unallocated memory. minAllocSize := offset + maxNodeSize - if cap(s.nodes) < minAllocSize { - allocSize := cap(s.nodes) * 2 + if uint64(cap(s.nodes)) < minAllocSize { + allocSize := uint64(cap(s.nodes)) * 2 if allocSize < minAllocSize { allocSize = minAllocSize } diff --git a/vendor/github.com/cockroachdb/pebble/internal/cache/clockpro.go b/vendor/github.com/cockroachdb/pebble/internal/cache/clockpro.go index de6b954..cdae6a9 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/cache/clockpro.go +++ b/vendor/github.com/cockroachdb/pebble/internal/cache/clockpro.go @@ -33,7 +33,7 @@ import ( type fileKey struct { // id is the namespace for fileNums. id uint64 - fileNum base.FileNum + fileNum base.DiskFileNum } type key struct { @@ -71,14 +71,12 @@ func (h Handle) Get() []byte { // Release releases the reference to the cache entry. func (h Handle) Release() { - if h.value != nil { - h.value.release() - } + h.value.release() } type shard struct { - hits int64 - misses int64 + hits atomic.Int64 + misses atomic.Int64 mu sync.RWMutex @@ -113,25 +111,25 @@ type shard struct { countTest int64 } -func (c *shard) Get(id uint64, fileNum base.FileNum, offset uint64) Handle { +func (c *shard) Get(id uint64, fileNum base.DiskFileNum, offset uint64) Handle { c.mu.RLock() var value *Value if e := c.blocks.Get(key{fileKey{id, fileNum}, offset}); e != nil { value = e.acquireValue() if value != nil { - atomic.StoreInt32(&e.referenced, 1) + e.referenced.Store(true) } } c.mu.RUnlock() if value == nil { - atomic.AddInt64(&c.misses, 1) + c.misses.Add(1) return Handle{} } - atomic.AddInt64(&c.hits, 1) + c.hits.Add(1) return Handle{value: value} } -func (c *shard) Set(id uint64, fileNum base.FileNum, offset uint64, value *Value) Handle { +func (c *shard) Set(id uint64, fileNum base.DiskFileNum, offset uint64, value *Value) Handle { if n := value.refs(); n != 1 { panic(fmt.Sprintf("pebble: Value has already been added to the cache: refs=%d", n)) } @@ -160,7 +158,7 @@ func (c *shard) Set(id uint64, fileNum base.FileNum, offset uint64, value *Value case e.peekValue() != nil: // cache entry was a hot or cold page e.setValue(value) - atomic.StoreInt32(&e.referenced, 1) + e.referenced.Store(true) delta := int64(len(value.buf)) - e.size e.size = int64(len(value.buf)) if e.ptype == etHot { @@ -176,7 +174,7 @@ func (c *shard) Set(id uint64, fileNum base.FileNum, offset uint64, value *Value // cache entry was a test page c.sizeTest -= e.size c.countTest-- - c.metaDel(e) + c.metaDel(e).release() c.metaCheck(e) e.size = int64(len(value.buf)) @@ -185,7 +183,7 @@ func (c *shard) Set(id uint64, fileNum base.FileNum, offset uint64, value *Value c.coldTarget = c.targetSize() } - atomic.StoreInt32(&e.referenced, 0) + e.referenced.Store(false) e.setValue(value) e.ptype = etHot if c.metaAdd(k, e) { @@ -222,7 +220,7 @@ func (c *shard) checkConsistency() { } // Delete deletes the cached value for the specified file and offset. -func (c *shard) Delete(id uint64, fileNum base.FileNum, offset uint64) { +func (c *shard) Delete(id uint64, fileNum base.DiskFileNum, offset uint64) { // The common case is there is nothing to delete, so do a quick check with // shared lock. k := key{fileKey{id, fileNum}, offset} @@ -233,37 +231,75 @@ func (c *shard) Delete(id uint64, fileNum base.FileNum, offset uint64) { return } - c.mu.Lock() - defer c.mu.Unlock() + var deletedValue *Value + func() { + c.mu.Lock() + defer c.mu.Unlock() - e := c.blocks.Get(k) - if e == nil { - return - } - c.metaEvict(e) - - c.checkConsistency() + e := c.blocks.Get(k) + if e == nil { + return + } + deletedValue = c.metaEvict(e) + c.checkConsistency() + }() + // Now that the mutex has been dropped, release the reference which will + // potentially free the memory associated with the previous cached value. + deletedValue.release() } // EvictFile evicts all of the cache values for the specified file. -func (c *shard) EvictFile(id uint64, fileNum base.FileNum) { - c.mu.Lock() - defer c.mu.Unlock() - +func (c *shard) EvictFile(id uint64, fileNum base.DiskFileNum) { fkey := key{fileKey{id, fileNum}, 0} + for c.evictFileRun(fkey) { + // Sched switch to give another goroutine an opportunity to acquire the + // shard mutex. + runtime.Gosched() + } +} + +func (c *shard) evictFileRun(fkey key) (moreRemaining bool) { + // If most of the file's blocks are held in the block cache, evicting all + // the blocks may take a while. We don't want to block the entire cache + // shard, forcing concurrent readers to wait until we're finished. We drop + // the mutex every [blocksPerMutexAcquisition] blocks to give other + // goroutines an opportunity to make progress. + const blocksPerMutexAcquisition = 5 + c.mu.Lock() + + // Releasing a value may result in free-ing it back to the memory allocator. + // This can have a nontrivial cost that we'd prefer to not pay while holding + // the shard mutex, so we collect the evicted values in a local slice and + // only release them in a defer after dropping the cache mutex. + var obsoleteValuesAlloc [blocksPerMutexAcquisition]*Value + obsoleteValues := obsoleteValuesAlloc[:0] + defer func() { + c.mu.Unlock() + for _, v := range obsoleteValues { + v.release() + } + }() + blocks := c.files.Get(fkey) if blocks == nil { - return - } - for b, n := blocks, (*entry)(nil); ; b = n { - n = b.fileLink.next - c.metaEvict(b) - if b == n { - break - } + // No blocks for this file. + return false } - c.checkConsistency() + // b is the current head of the doubly linked list, and n is the entry after b. + for b, n := blocks, (*entry)(nil); len(obsoleteValues) < cap(obsoleteValues); b = n { + n = b.fileLink.next + obsoleteValues = append(obsoleteValues, c.metaEvict(b)) + if b == n { + // b == n represents the case where b was the last entry remaining + // in the doubly linked list, which is why it pointed at itself. So + // no more entries left. + c.checkConsistency() + return false + } + } + // Exhausted blocksPerMutexAcquisition. + return true } func (c *shard) Free() { @@ -274,7 +310,7 @@ func (c *shard) Free() { // metaCheck call when the "invariants" build tag is specified. for c.handHot != nil { e := c.handHot - c.metaDel(c.handHot) + c.metaDel(c.handHot).release() e.free() } @@ -359,12 +395,14 @@ func (c *shard) metaAdd(key key, e *entry) bool { // Remove the entry from the cache. This removes the entry from the blocks map, // the files map, and ensures that hand{Hot,Cold,Test} are not pointing at the -// entry. -func (c *shard) metaDel(e *entry) { +// entry. Returns the deleted value that must be released, if any. +func (c *shard) metaDel(e *entry) (deletedValue *Value) { if value := e.peekValue(); value != nil { value.ref.trace("metaDel") } - e.setValue(nil) + // Remove the pointer to the value. + deletedValue = e.val + e.val = nil c.blocks.Delete(e.key) if entriesGoAllocated { @@ -396,6 +434,7 @@ func (c *shard) metaDel(e *entry) { } else { c.files.Put(fkey, next) } + return deletedValue } // Check that the specified entry is not referenced by the cache. @@ -455,7 +494,7 @@ func (c *shard) metaCheck(e *entry) { } } -func (c *shard) metaEvict(e *entry) { +func (c *shard) metaEvict(e *entry) (evictedValue *Value) { switch e.ptype { case etHot: c.sizeHot -= e.size @@ -467,9 +506,10 @@ func (c *shard) metaEvict(e *entry) { c.sizeTest -= e.size c.countTest-- } - c.metaDel(e) + evictedValue = c.metaDel(e) c.metaCheck(e) e.free() + return evictedValue } func (c *shard) evict() { @@ -491,8 +531,8 @@ func (c *shard) runHandCold(countColdDebug, sizeColdDebug int64) { e := c.handCold if e.ptype == etCold { - if atomic.LoadInt32(&e.referenced) == 1 { - atomic.StoreInt32(&e.referenced, 0) + if e.referenced.Load() { + e.referenced.Store(false) e.ptype = etHot c.sizeCold -= e.size c.countCold-- @@ -528,8 +568,8 @@ func (c *shard) runHandHot() { e := c.handHot if e.ptype == etHot { - if atomic.LoadInt32(&e.referenced) == 1 { - atomic.StoreInt32(&e.referenced, 0) + if e.referenced.Load() { + e.referenced.Store(false) } else { e.ptype = etCold c.sizeHot -= e.size @@ -564,7 +604,7 @@ func (c *shard) runHandTest() { if c.coldTarget < 0 { c.coldTarget = 0 } - c.metaDel(e) + c.metaDel(e).release() c.metaCheck(e) e.free() } @@ -587,7 +627,7 @@ type Metrics struct { // Cache implements Pebble's sharded block cache. The Clock-PRO algorithm is // used for page replacement // (http://static.usenix.org/event/usenix05/tech/general/full_papers/jiang/jiang_html/html.html). In -// order to provide better concurrency, 2 x NumCPUs shards are created, with +// order to provide better concurrency, 4 x NumCPUs shards are created, with // each shard being given 1/n of the target cache size. The Clock-PRO algorithm // is run independently on each shard. // @@ -621,9 +661,9 @@ type Metrics struct { // used in combination by specifying `-tags invariants,tracing`. Note that // "tracing" produces a significant slowdown, while "invariants" does not. type Cache struct { - refs int64 + refs atomic.Int64 maxSize int64 - idAlloc uint64 + idAlloc atomic.Uint64 shards []shard // Traces recorded by Cache.trace. Used for debugging. @@ -643,17 +683,44 @@ type Cache struct { // defer c.Unref() // d, err := pebble.Open(pebble.Options{Cache: c}) func New(size int64) *Cache { - return newShards(size, 2*runtime.GOMAXPROCS(0)) + // How many cache shards should we create? + // + // Note that the probability two processors will try to access the same + // shard at the same time increases superlinearly with the number of + // processors (Eg, consider the brithday problem where each CPU is a person, + // and each shard is a possible birthday). + // + // We could consider growing the number of shards superlinearly, but + // increasing the shard count may reduce the effectiveness of the caching + // algorithm if frequently-accessed blocks are insufficiently distributed + // across shards. If a shard's size is smaller than a single frequently + // scanned sstable, then the shard will be unable to hold the entire + // frequently-scanned table in memory despite other shards still holding + // infrequently accessed blocks. + // + // Experimentally, we've observed contention contributing to tail latencies + // at 2 shards per processor. For now we use 4 shards per processor, + // recognizing this may not be final word. + m := 4 * runtime.GOMAXPROCS(0) + + // In tests we can use large CPU machines with small cache sizes and have + // many caches in existence at a time. If sharding into m shards would + // produce too small shards, constrain the number of shards to 4. + const minimumShardSize = 4 << 20 // 4 MiB + if m > 4 && int(size)/m < minimumShardSize { + m = 4 + } + return newShards(size, m) } func newShards(size int64, shards int) *Cache { c := &Cache{ - refs: 1, maxSize: size, - idAlloc: 1, shards: make([]shard, shards), } - c.trace("alloc", c.refs) + c.refs.Store(1) + c.idAlloc.Store(1) + c.trace("alloc", c.refs.Load()) for i := range c.shards { c.shards[i] = shard{ maxSize: size / int64(len(c.shards)), @@ -669,7 +736,7 @@ func newShards(size int64, shards int) *Cache { // Note: this is a no-op if invariants are disabled or race is enabled. invariants.SetFinalizer(c, func(obj interface{}) { c := obj.(*Cache) - if v := atomic.LoadInt64(&c.refs); v != 0 { + if v := c.refs.Load(); v != 0 { c.tr.Lock() fmt.Fprintf(os.Stderr, "pebble: cache (%p) has non-zero reference count: %d\n", c, v) @@ -683,7 +750,7 @@ func newShards(size int64, shards int) *Cache { return c } -func (c *Cache) getShard(id uint64, fileNum base.FileNum, offset uint64) *shard { +func (c *Cache) getShard(id uint64, fileNum base.DiskFileNum, offset uint64) *shard { if id == 0 { panic("pebble: 0 cache ID is invalid") } @@ -698,10 +765,11 @@ func (c *Cache) getShard(id uint64, fileNum base.FileNum, offset uint64) *shard h ^= uint64(id & 0xff) id >>= 8 } + fileNumVal := uint64(fileNum.FileNum()) for i := 0; i < 8; i++ { h *= prime64 - h ^= uint64(fileNum & 0xff) - fileNum >>= 8 + h ^= uint64(fileNumVal) & 0xff + fileNumVal >>= 8 } for i := 0; i < 8; i++ { h *= prime64 @@ -715,7 +783,7 @@ func (c *Cache) getShard(id uint64, fileNum base.FileNum, offset uint64) *shard // Ref adds a reference to the cache. The cache only remains valid as long a // reference is maintained to it. func (c *Cache) Ref() { - v := atomic.AddInt64(&c.refs, 1) + v := c.refs.Add(1) if v <= 1 { panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) } @@ -724,7 +792,7 @@ func (c *Cache) Ref() { // Unref releases a reference on the cache. func (c *Cache) Unref() { - v := atomic.AddInt64(&c.refs, -1) + v := c.refs.Add(-1) c.trace("unref", v) switch { case v < 0: @@ -738,7 +806,7 @@ func (c *Cache) Unref() { // Get retrieves the cache value for the specified file and offset, returning // nil if no value is present. -func (c *Cache) Get(id uint64, fileNum base.FileNum, offset uint64) Handle { +func (c *Cache) Get(id uint64, fileNum base.DiskFileNum, offset uint64) Handle { return c.getShard(id, fileNum, offset).Get(id, fileNum, offset) } @@ -746,17 +814,17 @@ func (c *Cache) Get(id uint64, fileNum base.FileNum, offset uint64) Handle { // existing value if present. A Handle is returned which provides faster // retrieval of the cached value than Get (lock-free and avoidance of the map // lookup). The value must have been allocated by Cache.Alloc. -func (c *Cache) Set(id uint64, fileNum base.FileNum, offset uint64, value *Value) Handle { +func (c *Cache) Set(id uint64, fileNum base.DiskFileNum, offset uint64, value *Value) Handle { return c.getShard(id, fileNum, offset).Set(id, fileNum, offset, value) } // Delete deletes the cached value for the specified file and offset. -func (c *Cache) Delete(id uint64, fileNum base.FileNum, offset uint64) { +func (c *Cache) Delete(id uint64, fileNum base.DiskFileNum, offset uint64) { c.getShard(id, fileNum, offset).Delete(id, fileNum, offset) } // EvictFile evicts all of the cache values for the specified file. -func (c *Cache) EvictFile(id uint64, fileNum base.FileNum) { +func (c *Cache) EvictFile(id uint64, fileNum base.DiskFileNum) { if id == 0 { panic("pebble: 0 cache ID is invalid") } @@ -784,14 +852,14 @@ func (c *Cache) Size() int64 { // manually managed. The caller MUST either add the value to the cache (via // Cache.Set), or release the value (via Cache.Free). Failure to do so will // result in a memory leak. -func (c *Cache) Alloc(n int) *Value { +func Alloc(n int) *Value { return newValue(n) } // Free frees the specified value. The buffer associated with the value will // possibly be reused, making it invalid to use the buffer after calling // Free. Do not call Free on a value that has been added to the cache. -func (c *Cache) Free(v *Value) { +func Free(v *Value) { if n := v.refs(); n > 1 { panic(fmt.Sprintf("pebble: Value has been added to the cache: refs=%d", n)) } @@ -828,8 +896,8 @@ func (c *Cache) Metrics() Metrics { m.Count += int64(s.blocks.Count()) m.Size += s.sizeHot + s.sizeCold s.mu.RUnlock() - m.Hits += atomic.LoadInt64(&s.hits) - m.Misses += atomic.LoadInt64(&s.misses) + m.Hits += s.hits.Load() + m.Misses += s.misses.Load() } return m } @@ -837,5 +905,5 @@ func (c *Cache) Metrics() Metrics { // NewID returns a new ID to be used as a namespace for cached file // blocks. func (c *Cache) NewID() uint64 { - return atomic.AddUint64(&c.idAlloc, 1) + return c.idAlloc.Add(1) } diff --git a/vendor/github.com/cockroachdb/pebble/internal/cache/entry.go b/vendor/github.com/cockroachdb/pebble/internal/cache/entry.go index beb4891..a49fde6 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/cache/entry.go +++ b/vendor/github.com/cockroachdb/pebble/internal/cache/entry.go @@ -4,6 +4,8 @@ package cache +import "sync/atomic" + type entryType int8 const ( @@ -56,7 +58,7 @@ type entry struct { ptype entryType // referenced is atomically set to indicate that this entry has been accessed // since the last time one of the clock hands swept it. - referenced int32 + referenced atomic.Bool shard *shard // Reference count for the entry. The entry is freed when the reference count // drops to zero. @@ -137,9 +139,7 @@ func (e *entry) setValue(v *Value) { } old := e.val e.val = v - if old != nil { - old.release() - } + old.release() } func (e *entry) peekValue() *Value { diff --git a/vendor/github.com/cockroachdb/pebble/internal/cache/robin_hood.go b/vendor/github.com/cockroachdb/pebble/internal/cache/robin_hood.go index eb2c886..6e093fd 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/cache/robin_hood.go +++ b/vendor/github.com/cockroachdb/pebble/internal/cache/robin_hood.go @@ -24,7 +24,7 @@ func robinHoodHash(k key, shift uint32) uint32 { const m = 11400714819323198485 h := hashSeed h ^= k.id * m - h ^= uint64(k.fileNum) * m + h ^= uint64(k.fileNum.FileNum()) * m h ^= k.offset * m return uint32(h >> shift) } diff --git a/vendor/github.com/cockroachdb/pebble/internal/cache/value.go b/vendor/github.com/cockroachdb/pebble/internal/cache/value.go index c5c967a..6d2cae1 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/cache/value.go +++ b/vendor/github.com/cockroachdb/pebble/internal/cache/value.go @@ -40,7 +40,7 @@ func (v *Value) acquire() { } func (v *Value) release() { - if v.ref.release() { + if v != nil && v.ref.release() { v.free() } } diff --git a/vendor/github.com/cockroachdb/pebble/internal/constants/constants.go b/vendor/github.com/cockroachdb/pebble/internal/constants/constants.go new file mode 100644 index 0000000..8d9198c --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/internal/constants/constants.go @@ -0,0 +1,17 @@ +// Copyright 2023 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 constants + +const ( + // oneIf64Bit is 1 on 64-bit platforms and 0 on 32-bit platforms. + oneIf64Bit = ^uint(0) >> 63 + + // MaxUint32OrInt returns min(MaxUint32, MaxInt), i.e + // - MaxUint32 on 64-bit platforms; + // - MaxInt on 32-bit platforms. + // It is used when slices are limited to Uint32 on 64-bit platforms (the + // length limit for slices is naturally MaxInt on 32-bit platforms). + MaxUint32OrInt = (1<<31)<= key, _if_ the filterFunc (which has + // no knowledge of the seek key) mutated the span to start at a key greater + // than or equal to `key`. Detect this case and prev/invalidate the iter. + if span != nil && i.cmp(span.Start, key) >= 0 { + return i.Prev() + } + return span } // First implements FragmentIterator. diff --git a/vendor/github.com/cockroachdb/pebble/internal/keyspan/interleaving_iter.go b/vendor/github.com/cockroachdb/pebble/internal/keyspan/interleaving_iter.go index a507b75..6419821 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/keyspan/interleaving_iter.go +++ b/vendor/github.com/cockroachdb/pebble/internal/keyspan/interleaving_iter.go @@ -7,6 +7,7 @@ package keyspan import ( "fmt" + "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/invariants" ) @@ -111,6 +112,28 @@ type InterleavingIter struct { nextPrefixBuf []byte pointKey *base.InternalKey pointVal base.LazyValue + // err holds an iterator error from either pointIter or keyspanIter. It's + // reset to nil on seeks. An overview of error-handling mechanics: + // + // Whenever either pointIter or keyspanIter is respositioned and a nil + // key/span is returned, the code performing the positioning is responsible + // for checking the iterator's Error() value. This happens in savePoint and + // saveSpan[Forward,Backward]. + // + // Once i.err is non-nil, the computation of i.pos must set i.pos = + // posExhausted. This happens in compute[Smallest|Largest]Pos and + // [next|prev]Pos. Setting i.pos to posExhausted ensures we'll yield nil to + // the caller, which they'll interpret as a signal they must check Error(). + // + // INVARIANTS: + // i.err != nil => i.pos = posExhausted + err error + // prefix records the iterator's current prefix if the iterator is in prefix + // mode. During prefix mode, Pebble will truncate spans to the next prefix. + // If the iterator subsequently leaves prefix mode, the existing span cached + // in i.span must be invalidated because its bounds do not reflect the + // original span's true bounds. + prefix []byte // span holds the span at the keyspanIter's current position. If the span is // wholly contained within the iterator bounds, this span is directly // returned to the iterator consumer through Span(). If either bound needed @@ -126,31 +149,16 @@ type InterleavingIter struct { truncatedSpan Span truncated bool - // Keeping all of the bools together reduces the sizeof the struct. + // Keeping all of the bools/uint8s together reduces the sizeof the struct. - // spanCoversKey indicates whether the current span covers the last-returned - // key. - spanCoversKey bool - // pointKeyInterleaved indicates whether the current point key has been - // interleaved in the current direction. - pointKeyInterleaved bool - // keyspanInterleaved indicates whether or not the current span has been - // interleaved at its start key in the current direction. A span marker is - // interleaved when first passing over the start key. - // - // When iterating in the forward direction, the span start key is - // interleaved when the span first begins to cover the current iterator - // position. The keyspan iterator isn't advanced until the - // InterleavingIterator moves beyond the current span's end key. This field - // is used to remember that the span has already been interleaved and - // shouldn't be interleaved again. - // - // When iterating in the reverse direction, the span start key is - // interleaved immediately before the iterator will move to a key no longer - // be covered by the span. This field behaves analagously to - // pointKeyInterleaved and if true signals that we must Prev the keyspan - // iterator on the next Prev call. - keyspanInterleaved bool + // pos encodes the current position of the iterator: exhausted, on the point + // key, on a keyspan start, or on a keyspan end. + pos interleavePos + // withinSpan indicates whether the iterator is currently positioned within + // the bounds of the current span (i.span). withinSpan must be updated + // whenever the interleaving iterator's position enters or exits the bounds + // of a span. + withinSpan bool // spanMarkerTruncated is set by SeekGE/SeekPrefixGE calls that truncate a // span's start bound marker to the search key. It's returned to false on // the next repositioning of the keyspan iterator. @@ -158,39 +166,54 @@ type InterleavingIter struct { // maskSpanChangedCalled records whether or not the last call to // SpanMask.SpanChanged provided the current span (i.span) or not. maskSpanChangedCalled bool - // prefix records whether the iteator is in prefix mode. During prefix mode, - // Pebble will truncate spans to the next prefix. If the iterator - // subsequently leaves prefix mode, the existing span cached in i.span must - // be invalidated because its bounds do not reflect the original span's true - // bounds. - prefix bool // dir indicates the direction of iteration: forward (+1) or backward (-1) dir int8 } +// interleavePos indicates the iterator's current position. Note that both +// keyspanStart and keyspanEnd positions correspond to their user key boundaries +// with maximal sequence numbers. This means in the forward direction +// posKeyspanStart and posKeyspanEnd are always interleaved before a posPointKey +// with the same user key. +type interleavePos int8 + +const ( + posUninitialized interleavePos = iota + posExhausted + posPointKey + posKeyspanStart + posKeyspanEnd +) + // Assert that *InterleavingIter implements the InternalIterator interface. var _ base.InternalIterator = &InterleavingIter{} +// InterleavingIterOpts holds options configuring the behavior of a +// InterleavingIter. +type InterleavingIterOpts struct { + Mask SpanMask + LowerBound, UpperBound []byte +} + // Init initializes the InterleavingIter to interleave point keys from pointIter // with key spans from keyspanIter. // -// The point iterator must already have the provided bounds. Init does not -// propagate the bounds down the iterator stack. +// The point iterator must already have the bounds provided on opts. Init does +// not propagate the bounds down the iterator stack. func (i *InterleavingIter) Init( comparer *base.Comparer, pointIter base.InternalIterator, keyspanIter FragmentIterator, - mask SpanMask, - lowerBound, upperBound []byte, + opts InterleavingIterOpts, ) { *i = InterleavingIter{ cmp: comparer.Compare, comparer: comparer, pointIter: pointIter, keyspanIter: keyspanIter, - mask: mask, - lower: lowerBound, - upper: upperBound, + mask: opts.Mask, + lower: opts.LowerBound, + upper: opts.UpperBound, } } @@ -209,14 +232,14 @@ func (i *InterleavingIter) InitSeekGE( ) (*base.InternalKey, base.LazyValue) { i.dir = +1 i.clearMask() - i.prefix = prefix != nil - i.pointKey, i.pointVal = pointKey, pointValue - i.pointKeyInterleaved = false + i.prefix = prefix + i.savePoint(pointKey, pointValue) // NB: This keyspanSeekGE call will truncate the span to the seek key if // necessary. This truncation is important for cases where a switch to // combined iteration is made during a user-initiated SeekGE. i.keyspanSeekGE(key, prefix) - return i.interleaveForward(key, prefix) + i.computeSmallestPos() + return i.yieldPosition(key, i.nextPos) } // InitSeekLT may be called after Init but before any positioning method. @@ -234,10 +257,10 @@ func (i *InterleavingIter) InitSeekLT( ) (*base.InternalKey, base.LazyValue) { i.dir = -1 i.clearMask() - i.pointKey, i.pointVal = pointKey, pointValue - i.pointKeyInterleaved = false + i.savePoint(pointKey, pointValue) i.keyspanSeekLT(key) - return i.interleaveBackward() + i.computeLargestPos() + return i.yieldPosition(i.lower, i.prevPos) } // SeekGE implements (base.InternalIterator).SeekGE. @@ -254,10 +277,10 @@ func (i *InterleavingIter) InitSeekLT( func (i *InterleavingIter) SeekGE( key []byte, flags base.SeekGEFlags, ) (*base.InternalKey, base.LazyValue) { + i.err = nil i.clearMask() i.disablePrefixMode() - i.pointKey, i.pointVal = i.pointIter.SeekGE(key, flags) - i.pointKeyInterleaved = false + i.savePoint(i.pointIter.SeekGE(key, flags)) // We need to seek the keyspan iterator too. If the keyspan iterator was // already positioned at a span, we might be able to avoid the seek if the @@ -265,14 +288,15 @@ func (i *InterleavingIter) SeekGE( if i.span != nil && i.cmp(key, i.span.End) < 0 && i.cmp(key, i.span.Start) >= 0 { // We're seeking within the existing span's bounds. We still might need // truncate the span to the iterator's bounds. - i.checkForwardBound(nil /* prefix */) + i.saveSpanForward(i.span) i.savedKeyspan() } else { i.keyspanSeekGE(key, nil /* prefix */) } i.dir = +1 - return i.interleaveForward(key, nil /* prefix */) + i.computeSmallestPos() + return i.yieldPosition(key, i.nextPos) } // SeekPrefixGE implements (base.InternalIterator).SeekPrefixGE. @@ -289,10 +313,10 @@ func (i *InterleavingIter) SeekGE( func (i *InterleavingIter) SeekPrefixGE( prefix, key []byte, flags base.SeekGEFlags, ) (*base.InternalKey, base.LazyValue) { + i.err = nil i.clearMask() - i.pointKey, i.pointVal = i.pointIter.SeekPrefixGE(prefix, key, flags) - i.pointKeyInterleaved = false - i.prefix = true + i.prefix = prefix + i.savePoint(i.pointIter.SeekPrefixGE(prefix, key, flags)) // We need to seek the keyspan iterator too. If the keyspan iterator was // already positioned at a span, we might be able to avoid the seek if the @@ -321,7 +345,7 @@ func (i *InterleavingIter) SeekPrefixGE( if ei := i.comparer.Split(i.span.End); i.cmp(prefix, i.span.End[:ei]) < 0 { // We're seeking within the existing span's bounds. We still might need // truncate the span to the iterator's bounds. - i.checkForwardBound(prefix) + i.saveSpanForward(i.span) i.savedKeyspan() seekKeyspanIter = false } @@ -331,17 +355,18 @@ func (i *InterleavingIter) SeekPrefixGE( } i.dir = +1 - return i.interleaveForward(key, prefix) + i.computeSmallestPos() + return i.yieldPosition(key, i.nextPos) } // SeekLT implements (base.InternalIterator).SeekLT. func (i *InterleavingIter) SeekLT( key []byte, flags base.SeekLTFlags, ) (*base.InternalKey, base.LazyValue) { + i.err = nil i.clearMask() i.disablePrefixMode() - i.pointKey, i.pointVal = i.pointIter.SeekLT(key, flags) - i.pointKeyInterleaved = false + i.savePoint(i.pointIter.SeekLT(key, flags)) // We need to seek the keyspan iterator too. If the keyspan iterator was // already positioned at a span, we might be able to avoid the seek if the @@ -349,13 +374,13 @@ func (i *InterleavingIter) SeekLT( if i.span != nil && i.cmp(key, i.span.Start) > 0 && i.cmp(key, i.span.End) < 0 { // We're seeking within the existing span's bounds. We still might need // truncate the span to the iterator's bounds. - i.checkBackwardBound() + i.saveSpanBackward(i.span) // The span's start key is still not guaranteed to be less than key, // because of the bounds enforcement. Consider the following example: // // Bounds are set to [d,e). The user performs a SeekLT(d). The // FragmentIterator.SeekLT lands on a span [b,f). This span has a start - // key less than d, as expected. Above, checkBackwardBound truncates the + // key less than d, as expected. Above, saveSpanBackward truncates the // span to match the iterator's current bounds, modifying the span to // [d,e), which does not overlap the search space of [-∞, d). // @@ -371,33 +396,34 @@ func (i *InterleavingIter) SeekLT( } i.dir = -1 - return i.interleaveBackward() + i.computeLargestPos() + return i.yieldPosition(i.lower, i.prevPos) } // First implements (base.InternalIterator).First. func (i *InterleavingIter) First() (*base.InternalKey, base.LazyValue) { + i.err = nil i.clearMask() i.disablePrefixMode() - i.pointKey, i.pointVal = i.pointIter.First() - i.pointKeyInterleaved = false - i.span = i.keyspanIter.First() - i.checkForwardBound(nil /* prefix */) + i.savePoint(i.pointIter.First()) + i.saveSpanForward(i.keyspanIter.First()) i.savedKeyspan() i.dir = +1 - return i.interleaveForward(i.lower, nil /* prefix */) + i.computeSmallestPos() + return i.yieldPosition(i.lower, i.nextPos) } // Last implements (base.InternalIterator).Last. func (i *InterleavingIter) Last() (*base.InternalKey, base.LazyValue) { + i.err = nil i.clearMask() i.disablePrefixMode() - i.pointKey, i.pointVal = i.pointIter.Last() - i.pointKeyInterleaved = false - i.span = i.keyspanIter.Last() - i.checkBackwardBound() + i.savePoint(i.pointIter.Last()) + i.saveSpanBackward(i.keyspanIter.Last()) i.savedKeyspan() i.dir = -1 - return i.interleaveBackward() + i.computeLargestPos() + return i.yieldPosition(i.lower, i.prevPos) } // Next implements (base.InternalIterator).Next. @@ -413,70 +439,42 @@ func (i *InterleavingIter) Next() (*base.InternalKey, base.LazyValue) { i.clearMask() } - // The existing point key (denoted below with *) is either the last - // key we returned (the current iterator position): - // points: x (y*) z - // or the upcoming point key in the backward direction if we just - // returned a span start boundary key: - // points: x* z - // spans: ([y-?)) - // direction. Either way, we must move to the next point key. - switch { - case i.pointKey == nil && i.lower == nil: - i.pointKey, i.pointVal = i.pointIter.First() - case i.pointKey == nil && i.lower != nil: - i.pointKey, i.pointVal = i.pointIter.SeekGE(i.lower, base.SeekGEFlagsNone) - default: - i.pointKey, i.pointVal = i.pointIter.Next() - } - i.pointKeyInterleaved = false - - if i.span == nil { - // There was no span in the reverse direction, but there may be - // a span in the forward direction. - i.span = i.keyspanIter.Next() - i.checkForwardBound(nil /* prefix */) - i.savedKeyspan() - } else { - // Regardless of the current iterator state, we mark any existing - // span as interleaved when switching to forward iteration, - // justified below. - // - // If the point key is the last key returned: - // pointIter : ... (y) z ... - // keyspanIter : ... ([x - )) ... - // ^ - // The span's start key must be ≤ the point key, otherwise we'd have - // interleaved the span's start key. From a forward-iteration - // perspective, the span's start key is in the past and should be - // considered already-interleaved. - // - // If the span start boundary key is the last key returned: - // pointIter : ... (x) z ... - // keyspanIter : ... ([y - )) ... - // ^ - // i.span.Start is the key we last returned during reverse - // iteration. From the perspective of forward-iteration, its start - // key was just visited. - i.keyspanInterleaved = true + // When switching directions, iterator state corresponding to the + // current iterator position (as indicated by i.pos) is already correct. + // However any state that has yet to be interleaved describes a position + // behind the current iterator position and needs to be updated to + // describe the position ahead of the current iterator position. + switch i.pos { + case posExhausted: + // Nothing to do. The below nextPos call will move both the point + // key and span to their next positions and return + // MIN(point,s.Start). + case posPointKey: + // If we're currently on a point key, the below nextPos will + // correctly Next the point key iterator to the next point key. + // Do we need to move the span forwards? If the current span lies + // entirely behind the current key (!i.withinSpan), then we + // need to move it to the first span in the forward direction. + if !i.withinSpan { + i.saveSpanForward(i.keyspanIter.Next()) + i.savedKeyspan() + } + case posKeyspanStart: + i.withinSpan = true + // Since we're positioned on a Span, the pointIter is positioned + // entirely behind the current iterator position. Reposition it + // ahead of the current iterator position. + i.savePoint(i.pointIter.Next()) + case posKeyspanEnd: + // Since we're positioned on a Span, the pointIter is positioned + // entirely behind of the current iterator position. Reposition it + // ahead the current iterator position. + i.savePoint(i.pointIter.Next()) } + // Fallthrough to calling i.nextPos. } - - // Refresh the point key if the current point key has already been - // interleaved. - if i.pointKeyInterleaved { - i.pointKey, i.pointVal = i.pointIter.Next() - i.pointKeyInterleaved = false - } - // If we already interleaved the current span start key, and the point key - // is ≥ the span's end key, move to the next span. - if i.keyspanInterleaved && i.pointKey != nil && i.span != nil && - i.cmp(i.pointKey.UserKey, i.span.End) >= 0 { - i.span = i.keyspanIter.Next() - i.checkForwardBound(nil /* prefix */) - i.savedKeyspan() - } - return i.interleaveForward(i.lower, nil /* prefix */) + i.nextPos() + return i.yieldPosition(i.lower, i.nextPos) } // NextPrefix implements (base.InternalIterator).NextPrefix. @@ -485,22 +483,24 @@ func (i *InterleavingIter) NextPrefix(succKey []byte) (*base.InternalKey, base.L panic("pebble: cannot switch directions with NextPrefix") } - // Refresh the point key if the current point key has already been - // interleaved, OR if the next uninterleaved point key has the old suffix - // (it's less than succKey). - if i.pointKeyInterleaved || (i.pointKey != nil && i.cmp(i.pointKey.UserKey, succKey) < 0) { - i.pointKey, i.pointVal = i.pointIter.NextPrefix(succKey) - i.pointKeyInterleaved = false + switch i.pos { + case posExhausted: + return nil, base.LazyValue{} + case posPointKey: + i.savePoint(i.pointIter.NextPrefix(succKey)) + if i.withinSpan { + if i.pointKey == nil || i.cmp(i.span.End, i.pointKey.UserKey) <= 0 { + i.pos = posKeyspanEnd + } else { + i.pos = posPointKey + } + } else { + i.computeSmallestPos() + } + case posKeyspanStart, posKeyspanEnd: + i.nextPos() } - // If we already interleaved the current span start key, and the point key - // is ≥ the span's end key, move to the next span. - if i.keyspanInterleaved && i.pointKey != nil && i.span != nil && - i.cmp(i.pointKey.UserKey, i.span.End) >= 0 { - i.span = i.keyspanIter.Next() - i.checkForwardBound(nil) - i.savedKeyspan() - } - return i.interleaveForward(i.lower, nil) + return i.yieldPosition(i.lower, i.nextPos) } // Prev implements (base.InternalIterator).Prev. @@ -516,18 +516,34 @@ func (i *InterleavingIter) Prev() (*base.InternalKey, base.LazyValue) { i.clearMask() } - if i.keyspanInterleaved { - // The current span's start key has already been interleaved in the - // forward direction. The start key may have been interleaved a - // while ago, or it might've been interleaved at the current - // iterator position. If it was interleaved a while ago, the current - // span is still relevant and we should not move the keyspan - // iterator. - // - // If it was just interleaved at the current iterator position, the - // span start was the last key returned to the user. We should - // prev past it so we don't return it again, with an exception. - // Consider span [a, z) and this sequence of iterator calls: + // When switching directions, iterator state corresponding to the + // current iterator position (as indicated by i.pos) is already correct. + // However any state that has yet to be interleaved describes a position + // ahead of the current iterator position and needs to be updated to + // describe the position behind the current iterator position. + switch i.pos { + case posExhausted: + // Nothing to do. The below prevPos call will move both the point + // key and span to previous positions and return MAX(point, s.End). + case posPointKey: + // If we're currently on a point key, the point iterator is in the + // right place and the call to prevPos will correctly Prev the point + // key iterator to the previous point key. Do we need to move the + // span backwards? If the current span lies entirely ahead of the + // current key (!i.withinSpan), then we need to move it to the first + // span in the reverse direction. + if !i.withinSpan { + i.saveSpanBackward(i.keyspanIter.Prev()) + i.savedKeyspan() + } + case posKeyspanStart: + // Since we're positioned on a Span, the pointIter is positioned + // entirely ahead of the current iterator position. Reposition it + // behind the current iterator position. + i.savePoint(i.pointIter.Prev()) + // Without considering truncation of spans to seek keys, the keyspan + // iterator is already in the right place. But consider span [a, z) + // and this sequence of iterator calls: // // SeekGE('c') = c.RANGEKEYSET#72057594037927935 // Prev() = a.RANGEKEYSET#72057594037927935 @@ -535,176 +551,232 @@ func (i *InterleavingIter) Prev() (*base.InternalKey, base.LazyValue) { // If the current span's start key was last surfaced truncated due // to a SeekGE or SeekPrefixGE call, then it's still relevant in the // reverse direction with an untruncated start key. - // - // We can determine whether the last key returned was a point key by - // checking i.pointKeyInterleaved, because every Next/Prev will - // advance the point iterator and reset pointKeyInterleaved if it - // was. - if i.pointKeyInterleaved || i.spanMarkerTruncated { - // The last returned key was a point key, OR a truncated span - // marker key. Don't move, but re-save the span because it - // should no longer be considered truncated or interleaved. - i.savedKeyspan() - } else { - // The last returned key is this key's start boundary, so Prev - // past it so we don't return it again. - i.span = i.keyspanIter.Prev() - i.checkBackwardBound() - i.savedKeyspan() + if i.spanMarkerTruncated { + // When we fallthrough to calling prevPos, we want to move to + // MAX(point, span.Start). We cheat here by claiming we're + // currently on the end boundary, so that we'll move on to the + // untruncated start key if necessary. + i.pos = posKeyspanEnd } - } else { - // If the current span's start key has not been interleaved, then - // the span's start key is greater than the current iterator - // position (denoted in parenthesis), and the current span's start - // key is ahead of our iterator position. Move it to the previous - // span: - // points: (x*) - // span: [y-z)* - i.span = i.keyspanIter.Prev() - i.checkBackwardBound() + case posKeyspanEnd: + // Since we're positioned on a Span, the pointIter is positioned + // entirely ahead of the current iterator position. Reposition it + // behind the current iterator position. + i.savePoint(i.pointIter.Prev()) + } + + if i.spanMarkerTruncated { + // Save the keyspan again to clear truncation. i.savedKeyspan() } - - // The existing point key (denoted below with *) is either the last - // key we returned (the current iterator position): - // points: x (y*) z - // or the upcoming point key in the forward direction if we just - // returned a span start boundary key : - // points: x z* - // spans: ([y-?)) - // direction. Either way, we must move the point iterator backwards. - switch { - case i.pointKey == nil && i.upper == nil: - i.pointKey, i.pointVal = i.pointIter.Last() - case i.pointKey == nil && i.upper != nil: - i.pointKey, i.pointVal = i.pointIter.SeekLT(i.upper, base.SeekLTFlagsNone) - default: - i.pointKey, i.pointVal = i.pointIter.Prev() - } - i.pointKeyInterleaved = false + // Fallthrough to calling i.prevPos. } - - // Refresh the point key if we just returned the current point key. - if i.pointKeyInterleaved { - i.pointKey, i.pointVal = i.pointIter.Prev() - i.pointKeyInterleaved = false - } - // Refresh the span if we just returned the span's start boundary key. - if i.keyspanInterleaved { - i.span = i.keyspanIter.Prev() - i.checkBackwardBound() - i.savedKeyspan() - } - return i.interleaveBackward() + i.prevPos() + return i.yieldPosition(i.lower, i.prevPos) } -func (i *InterleavingIter) interleaveForward( - lowerBound []byte, prefix []byte, -) (*base.InternalKey, base.LazyValue) { - // This loop determines whether a point key or a span marker key should be - // interleaved on each iteration. If masking is disabled and the span is - // nonempty, this loop executes for exactly one iteration. If masking is - // enabled and a masked key is determined to be interleaved next, this loop - // continues until the interleaved key is unmasked. If a span's start key - // should be interleaved next, but the span is empty, the loop continues to - // the next key. - for { - // Check invariants. - if invariants.Enabled { - // INVARIANT: !pointKeyInterleaved - if i.pointKeyInterleaved { - panic("pebble: invariant violation: point key interleaved") - } - switch { - case i.span == nil: - case i.pointKey == nil: - default: - // INVARIANT: !keyspanInterleaved || pointKey < span.End - // The caller is responsible for advancing this span if it's already - // been interleaved and the span ends before the point key. - // Absolute positioning methods will never have already interleaved - // the span's start key, so only Next needs to handle the case where - // pointKey >= span.End. - if i.keyspanInterleaved && i.cmp(i.pointKey.UserKey, i.span.End) >= 0 { - panic("pebble: invariant violation: span interleaved, but point key >= span end") - } - } +// computeSmallestPos sets i.{pos,withinSpan} to: +// +// MIN(i.pointKey, i.span.Start) +func (i *InterleavingIter) computeSmallestPos() { + if i.err == nil { + if i.span != nil && (i.pointKey == nil || i.cmp(i.startKey(), i.pointKey.UserKey) <= 0) { + i.withinSpan = true + i.pos = posKeyspanStart + return } + i.withinSpan = false + if i.pointKey != nil { + i.pos = posPointKey + return + } + } + i.pos = posExhausted +} - // Interleave. +// computeLargestPos sets i.{pos,withinSpan} to: +// +// MAX(i.pointKey, i.span.End) +func (i *InterleavingIter) computeLargestPos() { + if i.err == nil { + if i.span != nil && (i.pointKey == nil || i.cmp(i.span.End, i.pointKey.UserKey) > 0) { + i.withinSpan = true + i.pos = posKeyspanEnd + return + } + i.withinSpan = false + if i.pointKey != nil { + i.pos = posPointKey + return + } + } + i.pos = posExhausted +} + +// nextPos advances the iterator one position in the forward direction. +func (i *InterleavingIter) nextPos() { + if invariants.Enabled { + defer func() { + if i.err != nil && i.pos != posExhausted { + panic(errors.AssertionFailedf("iterator has accumulated error but i.pos = %d", i.pos)) + } + }() + } + // NB: If i.err != nil or any of the positioning methods performed in this + // function result in i.err != nil, we must set i.pos = posExhausted. We + // perform this check explicitly here, but if any of the branches below + // advance either iterator, they must also check i.err and set posExhausted + // if necessary. + if i.err != nil { + i.pos = posExhausted + return + } + + switch i.pos { + case posExhausted: + i.savePoint(i.pointIter.Next()) + i.saveSpanForward(i.keyspanIter.Next()) + i.savedKeyspan() + i.computeSmallestPos() + case posPointKey: + i.savePoint(i.pointIter.Next()) + if i.err != nil { + i.pos = posExhausted + return + } + // If we're not currently within the span, we want to chose the + // MIN(pointKey,span.Start), which is exactly the calculation performed + // by computeSmallestPos. + if !i.withinSpan { + i.computeSmallestPos() + return + } + // i.withinSpan=true + // Since we previously were within the span, we want to choose the + // MIN(pointKey,span.End). switch { case i.span == nil: - // If we're out of spans, just return the point key. - return i.yieldPointKey(false /* covered */) + panic("i.withinSpan=true and i.span=nil") case i.pointKey == nil: - if i.pointKeyInterleaved { - panic("pebble: invariant violation: point key already interleaved") - } - // If we're out of point keys, we need to return a span marker. If - // the current span has already been interleaved, advance it. Since - // there are no more point keys, we don't need to worry about - // advancing past the current point key. - if i.keyspanInterleaved { - i.span = i.keyspanIter.Next() - i.checkForwardBound(prefix) - i.savedKeyspan() - if i.span == nil { - return i.yieldNil() - } - } - if i.span.Empty() { - i.keyspanInterleaved = true - continue - } - return i.yieldSyntheticSpanMarker(lowerBound) + // Since i.withinSpan=true, we step onto the end boundary of the + // keyspan. + i.pos = posKeyspanEnd default: - if i.cmp(i.pointKey.UserKey, i.startKey()) >= 0 { - // The span start key lies before the point key. If we haven't - // interleaved it, we should. - if !i.keyspanInterleaved { - if i.span.Empty() { - if i.pointKey != nil && i.cmp(i.pointKey.UserKey, i.span.End) >= 0 { - // Advance the keyspan iterator, as just flipping - // keyspanInterleaved would likely trip up the invariant check - // above. - i.span = i.keyspanIter.Next() - i.checkForwardBound(prefix) - i.savedKeyspan() - } else { - i.keyspanInterleaved = true - } - continue - } - return i.yieldSyntheticSpanMarker(lowerBound) - } + // i.withinSpan && i.pointKey != nil && i.span != nil + if i.cmp(i.span.End, i.pointKey.UserKey) <= 0 { + i.pos = posKeyspanEnd + } else { + i.pos = posPointKey + } + } + case posKeyspanStart: + // Either a point key or the span's end key comes next. + if i.pointKey != nil && i.cmp(i.pointKey.UserKey, i.span.End) < 0 { + i.pos = posPointKey + } else { + i.pos = posKeyspanEnd + } + case posKeyspanEnd: + i.saveSpanForward(i.keyspanIter.Next()) + i.savedKeyspan() + i.computeSmallestPos() + default: + panic(fmt.Sprintf("unexpected pos=%d", i.pos)) + } +} - // Otherwise, the span's start key is already interleaved and we - // need to return the point key. The current span necessarily - // must cover the point key: - // - // Since the span's start is less than or equal to the point - // key, the only way for this span to not cover the point would - // be if the span's end is less than or equal to the point. - // (For example span = [a, b), point key = c). - // - // However, the invariant at the beginning of the function - // guarantees that if: - // * we have both a point key and a span - // * and the span has already been interleaved - // => then the point key must be less than the span's end, and - // the point key must be covered by the current span. +// prevPos advances the iterator one position in the reverse direction. +func (i *InterleavingIter) prevPos() { + if invariants.Enabled { + defer func() { + if i.err != nil && i.pos != posExhausted { + panic(errors.AssertionFailedf("iterator has accumulated error but i.pos = %d", i.pos)) + } + }() + } + // NB: If i.err != nil or any of the positioning methods performed in this + // function result in i.err != nil, we must set i.pos = posExhausted. We + // perform this check explicitly here, but if any of the branches below + // advance either iterator, they must also check i.err and set posExhausted + // if necessary. + if i.err != nil { + i.pos = posExhausted + return + } - // The span covers the point key. If a SkipPoint hook is - // configured, ask it if we should skip this point key. - // - // But first, we may need to update the mask to the current span - // if we have stepped outside of the span last saved as a mask, - // so that the decision to skip is made with the correct - // knowledge of the covering span. - i.maybeUpdateMask(true /* covered */) + switch i.pos { + case posExhausted: + i.savePoint(i.pointIter.Prev()) + i.saveSpanBackward(i.keyspanIter.Prev()) + i.savedKeyspan() + i.computeLargestPos() + case posPointKey: + i.savePoint(i.pointIter.Prev()) + if i.err != nil { + i.pos = posExhausted + return + } + // If we're not currently covered by the span, we want to chose the + // MAX(pointKey,span.End), which is exactly the calculation performed + // by computeLargestPos. + if !i.withinSpan { + i.computeLargestPos() + return + } + switch { + case i.span == nil: + panic("withinSpan=true, but i.span == nil") + case i.pointKey == nil: + i.pos = posKeyspanEnd + default: + // i.withinSpan && i.pointKey != nil && i.span != nil + if i.cmp(i.span.Start, i.pointKey.UserKey) > 0 { + i.pos = posKeyspanStart + } else { + i.pos = posPointKey + } + } + case posKeyspanStart: + i.saveSpanBackward(i.keyspanIter.Prev()) + i.savedKeyspan() + i.computeLargestPos() + case posKeyspanEnd: + // Either a point key or the span's start key is previous. + if i.pointKey != nil && i.cmp(i.pointKey.UserKey, i.span.Start) >= 0 { + i.pos = posPointKey + } else { + i.pos = posKeyspanStart + } + default: + panic(fmt.Sprintf("unexpected pos=%d", i.pos)) + } +} - if i.mask != nil && i.mask.SkipPoint(i.pointKey.UserKey) { - if i.prefix { +func (i *InterleavingIter) yieldPosition( + lowerBound []byte, advance func(), +) (*base.InternalKey, base.LazyValue) { + // This loop returns the first visible position in the current iteration + // direction. Some positions are not visible and skipped. For example, if + // masking is enabled and the iterator is positioned over a masked point + // key, this loop skips the position. If a span's start key should be + // interleaved next, but the span is empty, the loop continues to the next + // key. Currently, span end keys are also always skipped, and are used only + // for maintaining internal state. + for { + switch i.pos { + case posExhausted: + return i.yieldNil() + case posPointKey: + if i.pointKey == nil { + panic("i.pointKey is nil") + } + + if i.mask != nil { + i.maybeUpdateMask() + if i.withinSpan && i.mask.SkipPoint(i.pointKey.UserKey) { + // The span covers the point key. If a SkipPoint hook is + // configured, ask it if we should skip this point key. + if i.prefix != nil { // During prefix-iteration node, once a point is masked, // all subsequent keys with the same prefix must also be // masked according to the key ordering. We can stop and @@ -721,126 +793,46 @@ func (i *InterleavingIter) interleaveForward( // TODO(jackson): If we thread a base.Comparer through to // InterleavingIter so that we have access to // ImmediateSuccessor, we could use NextPrefix. We'd need to - // tweak the SpanMask interface slightly, but it's probably - // worthwhile. + // tweak the SpanMask interface slightly. - i.pointKey, i.pointVal = i.pointIter.Next() - // We may have just invalidated the invariant that - // ensures the span's End is > the point key, so - // reestablish it before the next iteration. - if i.pointKey != nil && i.cmp(i.pointKey.UserKey, i.span.End) >= 0 { - i.span = i.keyspanIter.Next() - i.checkForwardBound(prefix) - i.savedKeyspan() - } + // Advance beyond the masked point key. + advance() continue } - - // Point key is unmasked but covered. - return i.yieldPointKey(true /* covered */) } - return i.yieldPointKey(false /* covered */) - } - } -} - -func (i *InterleavingIter) interleaveBackward() (*base.InternalKey, base.LazyValue) { - // This loop determines whether a point key or a span's start key should be - // interleaved on each iteration. If masking is disabled and the span is - // nonempty, this loop executes for exactly one iteration. If masking is - // enabled and a masked key is determined to be interleaved next, this loop - // continues until the interleaved key is unmasked. If a span's start key - // should be interleaved next, but the span is empty, the loop continues to - // the next key. - for { - // Check invariants. - if invariants.Enabled { - // INVARIANT: !pointKeyInterleaved - if i.pointKeyInterleaved { - panic("pebble: invariant violation: point key interleaved") - } - } - - // Interleave. - switch { - case i.span == nil: - // If we're out of spans, just return the point key. - return i.yieldPointKey(false /* covered */) - case i.pointKey == nil: - // If we're out of point keys, we need to return a span marker. + return i.yieldPointKey() + case posKeyspanEnd: + // Don't interleave end keys; just advance. + advance() + continue + case posKeyspanStart: + // Don't interleave an empty span. if i.span.Empty() { - i.span = i.keyspanIter.Prev() - i.checkBackwardBound() - i.savedKeyspan() + advance() continue } - return i.yieldSyntheticSpanMarker(i.lower) + return i.yieldSyntheticSpanMarker(lowerBound) default: - // If the span's start key is greater than the point key, return a - // marker for the span. - if i.cmp(i.startKey(), i.pointKey.UserKey) > 0 { - if i.span.Empty() { - i.span = i.keyspanIter.Prev() - i.checkBackwardBound() - i.savedKeyspan() - continue - } - return i.yieldSyntheticSpanMarker(i.lower) - } - // We have a span but it has not been interleaved and begins at a - // key equal to or before the current point key. The point key - // should be interleaved next, if it's not masked. - if i.cmp(i.pointKey.UserKey, i.span.End) < 0 { - // The span covers the point key. The point key might be masked - // too if masking is enabled. - // - // The span may have changed since the last time we updated the - // mask. Consider the following range-key masking scenario: - // - // |--------------) [b,d)@5 - // . c@4 . e@9 - // - // During reverse iteration when we step from e@9 to c@4, we - // enter the span [b,d)@5. Since end boundaries are not - // interleaved, the mask hasn't been updated with the span - // [b,d)@5 yet. We must update the mask before calling - // SkipPoint(c@4) to maintain the SpanMask contract and give the - // mask implementation an opportunity to build the state - // necessary to be able to determine whether [b,d)@5 masks c@4. - i.maybeUpdateMask(true /* covered */) - - // The span covers the point key. If a SkipPoint hook is - // configured, ask it if we should skip this point key. - if i.mask != nil && i.mask.SkipPoint(i.pointKey.UserKey) { - i.pointKey, i.pointVal = i.pointIter.Prev() - continue - } - - // Point key is unmasked but covered. - return i.yieldPointKey(true /* covered */) - } - return i.yieldPointKey(false /* covered */) + panic(fmt.Sprintf("unexpected interleavePos=%d", i.pos)) } } } // keyspanSeekGE seeks the keyspan iterator to the first span covering a key ≥ k. func (i *InterleavingIter) keyspanSeekGE(k []byte, prefix []byte) { - i.span = i.keyspanIter.SeekGE(k) - i.checkForwardBound(prefix) + i.saveSpanForward(i.keyspanIter.SeekGE(k)) i.savedKeyspan() } // keyspanSeekLT seeks the keyspan iterator to the last span covering a key < k. func (i *InterleavingIter) keyspanSeekLT(k []byte) { - i.span = i.keyspanIter.SeekLT(k) - i.checkBackwardBound() + i.saveSpanBackward(i.keyspanIter.SeekLT(k)) // The current span's start key is not guaranteed to be less than key, // because of the bounds enforcement. Consider the following example: // // Bounds are set to [d,e). The user performs a SeekLT(d). The // FragmentIterator.SeekLT lands on a span [b,f). This span has a start key - // less than d, as expected. Above, checkBackwardBound truncates the span to + // less than d, as expected. Above, saveSpanBackward truncates the span to // match the iterator's current bounds, modifying the span to [d,e), which // does not overlap the search space of [-∞, d). // @@ -852,12 +844,21 @@ func (i *InterleavingIter) keyspanSeekLT(k []byte) { i.savedKeyspan() } -func (i *InterleavingIter) checkForwardBound(prefix []byte) { +func (i *InterleavingIter) saveSpanForward(span *Span) { + i.span = span i.truncated = false i.truncatedSpan = Span{} if i.span == nil { + i.err = firstError(i.err, i.keyspanIter.Error()) return } + if invariants.Enabled { + if err := i.keyspanIter.Error(); err != nil { + panic(errors.WithSecondaryError( + errors.AssertionFailedf("pebble: %T keyspan iterator returned non-nil span %s while iter has error", i.keyspanIter, i.span), + err)) + } + } // Check the upper bound if we have one. if i.upper != nil && i.cmp(i.span.Start, i.upper) >= 0 { i.span = nil @@ -886,15 +887,15 @@ func (i *InterleavingIter) checkForwardBound(prefix []byte) { } // If this is a part of a SeekPrefixGE call, we may also need to truncate to // the prefix's bounds. - if prefix != nil { + if i.prefix != nil { if !i.truncated { i.truncated = true i.truncatedSpan = *i.span } - if i.cmp(prefix, i.truncatedSpan.Start) > 0 { - i.truncatedSpan.Start = prefix + if i.cmp(i.prefix, i.truncatedSpan.Start) > 0 { + i.truncatedSpan.Start = i.prefix } - i.nextPrefixBuf = i.comparer.ImmediateSuccessor(i.nextPrefixBuf[:0], prefix) + i.nextPrefixBuf = i.comparer.ImmediateSuccessor(i.nextPrefixBuf[:0], i.prefix) if i.truncated && i.cmp(i.nextPrefixBuf, i.truncatedSpan.End) < 0 { i.truncatedSpan.End = i.nextPrefixBuf } @@ -905,12 +906,22 @@ func (i *InterleavingIter) checkForwardBound(prefix []byte) { } } -func (i *InterleavingIter) checkBackwardBound() { +func (i *InterleavingIter) saveSpanBackward(span *Span) { + i.span = span i.truncated = false i.truncatedSpan = Span{} if i.span == nil { + i.err = firstError(i.err, i.keyspanIter.Error()) return } + if invariants.Enabled { + if err := i.keyspanIter.Error(); err != nil { + panic(errors.WithSecondaryError( + errors.AssertionFailedf("pebble: %T keyspan iterator returned non-nil span %s while iter has error", i.keyspanIter, i.span), + err)) + } + } + // Check the lower bound if we have one. if i.lower != nil && i.cmp(i.span.End, i.lower) <= 0 { i.span = nil @@ -943,15 +954,12 @@ func (i *InterleavingIter) checkBackwardBound() { } func (i *InterleavingIter) yieldNil() (*base.InternalKey, base.LazyValue) { - i.spanCoversKey = false + i.withinSpan = false i.clearMask() return i.verify(nil, base.LazyValue{}) } -func (i *InterleavingIter) yieldPointKey(covered bool) (*base.InternalKey, base.LazyValue) { - i.pointKeyInterleaved = true - i.spanCoversKey = covered - i.maybeUpdateMask(covered) +func (i *InterleavingIter) yieldPointKey() (*base.InternalKey, base.LazyValue) { return i.verify(i.pointKey, i.pointVal) } @@ -960,8 +968,6 @@ func (i *InterleavingIter) yieldSyntheticSpanMarker( ) (*base.InternalKey, base.LazyValue) { i.spanMarker.UserKey = i.startKey() i.spanMarker.Trailer = base.MakeTrailer(base.InternalKeySeqNumMax, i.span.Keys[0].Kind()) - i.keyspanInterleaved = true - i.spanCoversKey = true // Truncate the key we return to our lower bound if we have one. Note that // we use the lowerBound function parameter, not i.lower. The lowerBound @@ -992,13 +998,13 @@ func (i *InterleavingIter) yieldSyntheticSpanMarker( i.spanMarker.UserKey = i.keyBuf i.spanMarkerTruncated = true } - i.maybeUpdateMask(true /* covered */) + i.maybeUpdateMask() return i.verify(&i.spanMarker, base.LazyValue{}) } func (i *InterleavingIter) disablePrefixMode() { - if i.prefix { - i.prefix = false + if i.prefix != nil { + i.prefix = nil // Clear the existing span. It may not hold the true end bound of the // underlying span. i.span = nil @@ -1012,52 +1018,42 @@ func (i *InterleavingIter) verify( // production builds elide this entire function. if invariants.Enabled { switch { - case k != nil && !i.keyspanInterleaved && !i.pointKeyInterleaved: - panic("pebble: invariant violation: both keys marked as noninterleaved") - case i.dir == -1 && k != nil && i.keyspanInterleaved == i.pointKeyInterleaved: - // During reverse iteration, if we're returning a key, either the span's - // start key must have been interleaved OR the current point key value - // is being returned, not both. - // - // This invariant holds because in reverse iteration the start key of the - // span behaves like a point. Once the start key is interleaved, we move - // the keyspan iterator to the previous span. - panic(fmt.Sprintf("pebble: invariant violation: interleaving (point %t, span %t)", - i.pointKeyInterleaved, i.keyspanInterleaved)) case i.dir == -1 && i.spanMarkerTruncated: panic("pebble: invariant violation: truncated span key in reverse iteration") case k != nil && i.lower != nil && i.cmp(k.UserKey, i.lower) < 0: panic("pebble: invariant violation: key < lower bound") case k != nil && i.upper != nil && i.cmp(k.UserKey, i.upper) >= 0: panic("pebble: invariant violation: key ≥ upper bound") - case i.span != nil && k != nil && i.mask != nil && i.pointKeyInterleaved && - i.cmp(k.UserKey, i.span.Start) >= 0 && i.cmp(k.UserKey, i.span.End) < 0 && i.mask.SkipPoint(k.UserKey): - panic("pebble: invariant violation: point key eligible for skipping returned") + case i.err != nil && k != nil: + panic("pebble: invariant violation: accumulated error swallowed") + case i.err == nil && i.pointIter.Error() != nil: + panic("pebble: invariant violation: pointIter swallowed") + case i.err == nil && i.keyspanIter.Error() != nil: + panic("pebble: invariant violation: keyspanIter error swallowed") } } return k, v } func (i *InterleavingIter) savedKeyspan() { - i.keyspanInterleaved = false i.spanMarkerTruncated = false i.maskSpanChangedCalled = false } -// maybeUpdateMask updates the current mask, if a mask is configured and -// the mask hasn't been updated with the current keyspan yet. -func (i *InterleavingIter) maybeUpdateMask(covered bool) { - if i.mask != nil { - if !covered || i.span.Empty() { - i.clearMask() - } else if !i.maskSpanChangedCalled { - if i.truncated { - i.mask.SpanChanged(&i.truncatedSpan) - } else { - i.mask.SpanChanged(i.span) - } - i.maskSpanChangedCalled = true - } +// updateMask updates the current mask, if a mask is configured and the mask +// hasn't been updated with the current keyspan yet. +func (i *InterleavingIter) maybeUpdateMask() { + switch { + case i.mask == nil, i.maskSpanChangedCalled: + return + case !i.withinSpan || i.span.Empty(): + i.clearMask() + case i.truncated: + i.mask.SpanChanged(&i.truncatedSpan) + i.maskSpanChangedCalled = true + default: + i.mask.SpanChanged(i.span) + i.maskSpanChangedCalled = true } } @@ -1077,6 +1073,20 @@ func (i *InterleavingIter) startKey() []byte { return i.span.Start } +func (i *InterleavingIter) savePoint(key *base.InternalKey, value base.LazyValue) { + i.pointKey, i.pointVal = key, value + if key == nil { + i.err = firstError(i.err, i.pointIter.Error()) + } + if invariants.Enabled { + if err := i.pointIter.Error(); key != nil && err != nil { + panic(errors.WithSecondaryError( + errors.AssertionFailedf("pebble: %T point iterator returned non-nil key %q while iter has error", i.pointIter, key), + err)) + } + } +} + // Span returns the span covering the last key returned, if any. A span key is // considered to 'cover' a key if the key falls within the span's user key // bounds. The returned span is owned by the InterleavingIter. The caller is @@ -1084,13 +1094,12 @@ func (i *InterleavingIter) startKey() []byte { // // Span will never return an invalid or empty span. func (i *InterleavingIter) Span() *Span { - if !i.spanCoversKey || i.span.Empty() { + if !i.withinSpan || len(i.span.Keys) == 0 { return nil } else if i.truncated { return &i.truncatedSpan - } else { - return i.span } + return i.span } // SetBounds implements (base.InternalIterator).SetBounds. @@ -1111,7 +1120,7 @@ func (i *InterleavingIter) Invalidate() { // Error implements (base.InternalIterator).Error. func (i *InterleavingIter) Error() error { - return firstError(i.pointIter.Error(), i.keyspanIter.Error()) + return i.err } // Close implements (base.InternalIterator).Close. diff --git a/vendor/github.com/cockroachdb/pebble/internal/keyspan/iter.go b/vendor/github.com/cockroachdb/pebble/internal/keyspan/iter.go index 4798561..7f8ceb8 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/keyspan/iter.go +++ b/vendor/github.com/cockroachdb/pebble/internal/keyspan/iter.go @@ -51,6 +51,8 @@ type FragmentIterator interface { Prev() *Span // Error returns any accumulated error. + // + // TODO(jackson): Lift errors into return values on the positioning methods. Error() error // Close closes the iterator and returns any accumulated error. Exhausting @@ -62,7 +64,7 @@ type FragmentIterator interface { // TableNewSpanIter creates a new iterator for range key spans for the given // file. -type TableNewSpanIter func(file *manifest.FileMetadata, iterOptions *SpanIterOptions) (FragmentIterator, error) +type TableNewSpanIter func(file *manifest.FileMetadata, iterOptions SpanIterOptions) (FragmentIterator, error) // SpanIterOptions is a subset of IterOptions that are necessary to instantiate // per-sstable span iterators. diff --git a/vendor/github.com/cockroachdb/pebble/internal/keyspan/level_iter.go b/vendor/github.com/cockroachdb/pebble/internal/keyspan/level_iter.go index 047c6a2..6dd7ac6 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/keyspan/level_iter.go +++ b/vendor/github.com/cockroachdb/pebble/internal/keyspan/level_iter.go @@ -98,7 +98,7 @@ func (l *LevelIter) Init( ) { l.err = nil l.level = level - l.tableOpts.RangeKeyFilters = opts.RangeKeyFilters + l.tableOpts = opts l.cmp = cmp l.iterFile = nil l.newIter = newIter @@ -179,7 +179,7 @@ func (l *LevelIter) loadFile(file *manifest.FileMetadata, dir int) loadFileRetur return noFileLoaded } if indicator != fileAlreadyLoaded { - l.iter, l.err = l.newIter(file, &l.tableOpts) + l.iter, l.err = l.newIter(file, l.tableOpts) indicator = newFileLoaded } if l.err != nil { diff --git a/vendor/github.com/cockroachdb/pebble/internal/keyspan/merging_iter.go b/vendor/github.com/cockroachdb/pebble/internal/keyspan/merging_iter.go index 2476024..c73ba59 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/keyspan/merging_iter.go +++ b/vendor/github.com/cockroachdb/pebble/internal/keyspan/merging_iter.go @@ -25,49 +25,6 @@ import ( // MergingIter implementation, but will require a bit of plumbing to thread the // Equal function. -// Transformer defines a transformation to be applied to a Span. -type Transformer interface { - // Transform takes a Span as input and writes the transformed Span to the - // provided output *Span pointer. The output Span's Keys slice may be reused - // by Transform to reduce allocations. - Transform(cmp base.Compare, in Span, out *Span) error -} - -// The TransformerFunc type is an adapter to allow the use of ordinary functions -// as Transformers. If f is a function with the appropriate signature, -// TransformerFunc(f) is a Transformer that calls f. -type TransformerFunc func(base.Compare, Span, *Span) error - -// Transform calls f(cmp, in, out). -func (tf TransformerFunc) Transform(cmp base.Compare, in Span, out *Span) error { - return tf(cmp, in, out) -} - -var noopTransform Transformer = TransformerFunc(func(_ base.Compare, s Span, dst *Span) error { - dst.Start, dst.End = s.Start, s.End - dst.Keys = append(dst.Keys[:0], s.Keys...) - return nil -}) - -// VisibleTransform filters keys that are invisible at the provided snapshot -// sequence number. -func VisibleTransform(snapshot uint64) Transformer { - return TransformerFunc(func(_ base.Compare, s Span, dst *Span) error { - dst.Start, dst.End = s.Start, s.End - dst.Keys = dst.Keys[:0] - for _, k := range s.Keys { - // NB: The InternalKeySeqNumMax value is used for the batch snapshot - // because a batch's visible span keys are filtered when they're - // fragmented. There's no requirement to enforce visibility at - // iteration time. - if base.Visible(k.SeqNum(), snapshot, base.InternalKeySeqNumMax) { - dst.Keys = append(dst.Keys, k) - } - } - return nil - }) -} - // MergingIter merges spans across levels of the LSM, exposing an iterator over // spans that yields sets of spans fragmented at unique user key boundaries. // diff --git a/vendor/github.com/cockroachdb/pebble/internal/keyspan/transformer.go b/vendor/github.com/cockroachdb/pebble/internal/keyspan/transformer.go new file mode 100644 index 0000000..b5e8735 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/internal/keyspan/transformer.go @@ -0,0 +1,50 @@ +// Copyright 2023 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 keyspan + +import "github.com/cockroachdb/pebble/internal/base" + +// Transformer defines a transformation to be applied to a Span. +type Transformer interface { + // Transform takes a Span as input and writes the transformed Span to the + // provided output *Span pointer. The output Span's Keys slice may be reused + // by Transform to reduce allocations. + Transform(cmp base.Compare, in Span, out *Span) error +} + +// The TransformerFunc type is an adapter to allow the use of ordinary functions +// as Transformers. If f is a function with the appropriate signature, +// TransformerFunc(f) is a Transformer that calls f. +type TransformerFunc func(base.Compare, Span, *Span) error + +// Transform calls f(cmp, in, out). +func (tf TransformerFunc) Transform(cmp base.Compare, in Span, out *Span) error { + return tf(cmp, in, out) +} + +var noopTransform Transformer = TransformerFunc(func(_ base.Compare, s Span, dst *Span) error { + dst.Start, dst.End = s.Start, s.End + dst.Keys = append(dst.Keys[:0], s.Keys...) + return nil +}) + +// VisibleTransform filters keys that are invisible at the provided snapshot +// sequence number. +func VisibleTransform(snapshot uint64) Transformer { + return TransformerFunc(func(_ base.Compare, s Span, dst *Span) error { + dst.Start, dst.End = s.Start, s.End + dst.Keys = dst.Keys[:0] + for _, k := range s.Keys { + // NB: The InternalKeySeqNumMax value is used for the batch snapshot + // because a batch's visible span keys are filtered when they're + // fragmented. There's no requirement to enforce visibility at + // iteration time. + if base.Visible(k.SeqNum(), snapshot, base.InternalKeySeqNumMax) { + dst.Keys = append(dst.Keys, k) + } + } + return nil + }) +} diff --git a/vendor/github.com/cockroachdb/pebble/internal/keyspan/truncate.go b/vendor/github.com/cockroachdb/pebble/internal/keyspan/truncate.go index af3dddd..c0e609b 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/keyspan/truncate.go +++ b/vendor/github.com/cockroachdb/pebble/internal/keyspan/truncate.go @@ -10,13 +10,18 @@ import "github.com/cockroachdb/pebble/internal/base" // truncated to be contained within the range [lower, upper). If start and end // are specified, filter out any spans that are completely outside those bounds. func Truncate( - cmp base.Compare, iter FragmentIterator, lower, upper []byte, start, end *base.InternalKey, + cmp base.Compare, + iter FragmentIterator, + lower, upper []byte, + start, end *base.InternalKey, + panicOnUpperTruncate bool, ) FragmentIterator { return Filter(iter, func(in *Span, out *Span) (keep bool) { out.Start, out.End = in.Start, in.End out.Keys = append(out.Keys[:0], in.Keys...) - // Ignore this span if it lies completely outside [start, end]. + // Ignore this span if it lies completely outside start, end. Note that + // end endInclusive indicated whether end is inclusive. // // The comparison between s.End and start is by user key only, as // the span is exclusive at s.End, so comparing by user keys @@ -48,13 +53,21 @@ func Truncate( // Wholly within the end bound. Keep it. } } + + var truncated bool // Truncate the bounds to lower and upper. if cmp(in.Start, lower) < 0 { out.Start = lower } if cmp(in.End, upper) > 0 { + truncated = true out.End = upper } + + if panicOnUpperTruncate && truncated { + panic("pebble: upper bound should not be truncated") + } + return !out.Empty() && cmp(out.Start, out.End) < 0 - }) + }, cmp) } diff --git a/vendor/github.com/cockroachdb/pebble/internal/manifest/btree.go b/vendor/github.com/cockroachdb/pebble/internal/manifest/btree.go index 66464c9..6e56af8 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/manifest/btree.go +++ b/vendor/github.com/cockroachdb/pebble/internal/manifest/btree.go @@ -108,7 +108,7 @@ type annotation struct { } type leafNode struct { - ref int32 + ref atomic.Int32 count int16 leaf bool // subtreeCount holds the count of files in the entire subtree formed by @@ -139,13 +139,13 @@ func leafToNode(ln *leafNode) *node { func newLeafNode() *node { n := leafToNode(new(leafNode)) n.leaf = true - n.ref = 1 + n.ref.Store(1) return n } func newNode() *node { n := new(node) - n.ref = 1 + n.ref.Store(1) return n } @@ -159,7 +159,7 @@ func newNode() *node { // When a node is cloned, the provided pointer will be redirected to the new // mutable node. func mut(n **node) *node { - if atomic.LoadInt32(&(*n).ref) == 1 { + if (*n).ref.Load() == 1 { // Exclusive ownership. Can mutate in place. // Whenever a node will be mutated, reset its annotations to be marked @@ -186,7 +186,7 @@ func mut(n **node) *node { // incRef acquires a reference to the node. func (n *node) incRef() { - atomic.AddInt32(&n.ref, 1) + n.ref.Add(1) } // decRef releases a reference to the node. If requested, the method will unref @@ -195,8 +195,8 @@ func (n *node) incRef() { // new nodes pass contentsToo=false to preserve existing reference counts during // operations that should yield a net-zero change to descendant refcounts. // When a node is released, its contained files are dereferenced. -func (n *node) decRef(contentsToo bool, obsolete *[]*FileMetadata) { - if atomic.AddInt32(&n.ref, -1) > 0 { +func (n *node) decRef(contentsToo bool, obsolete *[]*FileBacking) { + if n.ref.Add(-1) > 0 { // Other references remain. Can't free. return } @@ -207,7 +207,7 @@ func (n *node) decRef(contentsToo bool, obsolete *[]*FileMetadata) { // nodes, and they want to preserve the existing reference count. if contentsToo { for _, f := range n.items[:n.count] { - if atomic.AddInt32(&f.Refs, -1) == 0 { + if f.Unref() == 0 { // There are two sources of node dereferences: tree mutations // and Version dereferences. Files should only be made obsolete // during Version dereferences, during which `obsolete` will be @@ -215,7 +215,13 @@ func (n *node) decRef(contentsToo bool, obsolete *[]*FileMetadata) { if obsolete == nil { panic(fmt.Sprintf("file metadata %s dereferenced to zero during tree mutation", f.FileNum)) } - *obsolete = append(*obsolete, f) + // Reference counting is performed on the FileBacking. In the case + // of a virtual sstable, this reference counting is performed on + // a FileBacking which is shared by every single virtual sstable + // with the same backing sstable. If the reference count hits 0, + // then we know that the FileBacking won't be required by any + // sstable in Pebble, and that the backing sstable can be deleted. + *obsolete = append(*obsolete, f.FileBacking) } } if !n.leaf { @@ -241,7 +247,7 @@ func (n *node) clone() *node { c.subtreeCount = n.subtreeCount // Increase the refcount of each contained item. for _, f := range n.items[:n.count] { - atomic.AddInt32(&f.Refs, 1) + f.Ref() } if !c.leaf { // Copy children and increase each refcount. @@ -761,8 +767,8 @@ type btree struct { // Release dereferences and clears the root node of the btree, removing all // items from the btree. In doing so, it decrements contained file counts. -// It returns a slice of newly obsolete files, if any. -func (t *btree) Release() (obsolete []*FileMetadata) { +// It returns a slice of newly obsolete backing files, if any. +func (t *btree) Release() (obsolete []*FileBacking) { if t.root != nil { t.root.decRef(true /* contentsToo */, &obsolete) t.root = nil @@ -800,7 +806,7 @@ func (t *btree) Delete(item *FileMetadata) (obsolete bool) { return false } if out := mut(&t.root).Remove(t.cmp, item); out != nil { - obsolete = atomic.AddInt32(&out.Refs, -1) == 0 + obsolete = out.Unref() == 0 } if invariants.Enabled { t.root.verifyInvariants() @@ -832,7 +838,7 @@ func (t *btree) Insert(item *FileMetadata) error { newRoot.subtreeCount = t.root.subtreeCount + splitNode.subtreeCount + 1 t.root = newRoot } - atomic.AddInt32(&item.Refs, 1) + item.Ref() err := mut(&t.root).Insert(t.cmp, item) if invariants.Enabled { t.root.verifyInvariants() diff --git a/vendor/github.com/cockroachdb/pebble/internal/manifest/level_metadata.go b/vendor/github.com/cockroachdb/pebble/internal/manifest/level_metadata.go index 3e1d26d..d48e277 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/manifest/level_metadata.go +++ b/vendor/github.com/cockroachdb/pebble/internal/manifest/level_metadata.go @@ -15,20 +15,28 @@ import ( // LevelMetadata contains metadata for all of the files within // a level of the LSM. type LevelMetadata struct { - level int - tree btree + level int + totalSize uint64 + // NumVirtual is the number of virtual sstables in the level. + NumVirtual uint64 + // VirtualSize is the size of the virtual sstables in the level. + VirtualSize uint64 + tree btree } // clone makes a copy of the level metadata, implicitly increasing the ref // count of every file contained within lm. func (lm *LevelMetadata) clone() LevelMetadata { return LevelMetadata{ - level: lm.level, - tree: lm.tree.Clone(), + level: lm.level, + totalSize: lm.totalSize, + NumVirtual: lm.NumVirtual, + VirtualSize: lm.VirtualSize, + tree: lm.tree.Clone(), } } -func (lm *LevelMetadata) release() (obsolete []*FileMetadata) { +func (lm *LevelMetadata) release() (obsolete []*FileBacking) { return lm.tree.Release() } @@ -40,6 +48,13 @@ func makeLevelMetadata(cmp Compare, level int, files []*FileMetadata) LevelMetad var lm LevelMetadata lm.level = level lm.tree, _ = makeBTree(bcmp, files) + for _, f := range files { + lm.totalSize += f.Size + if f.Virtual { + lm.NumVirtual++ + lm.VirtualSize += f.Size + } + } return lm } @@ -52,6 +67,27 @@ func makeBTree(cmp btreeCmp, files []*FileMetadata) (btree, LevelSlice) { return t, newLevelSlice(t.Iter()) } +func (lm *LevelMetadata) insert(f *FileMetadata) error { + if err := lm.tree.Insert(f); err != nil { + return err + } + lm.totalSize += f.Size + if f.Virtual { + lm.NumVirtual++ + lm.VirtualSize += f.Size + } + return nil +} + +func (lm *LevelMetadata) remove(f *FileMetadata) bool { + lm.totalSize -= f.Size + if f.Virtual { + lm.NumVirtual-- + lm.VirtualSize -= f.Size + } + return lm.tree.Delete(f) +} + // Empty indicates whether there are any files in the level. func (lm *LevelMetadata) Empty() bool { return lm.tree.Count() == 0 @@ -62,6 +98,11 @@ func (lm *LevelMetadata) Len() int { return lm.tree.Count() } +// Size returns the cumulative size of all the files within the level. +func (lm *LevelMetadata) Size() uint64 { + return lm.totalSize +} + // Iter constructs a LevelIterator over the entire level. func (lm *LevelMetadata) Iter() LevelIterator { return LevelIterator{iter: lm.tree.Iter()} @@ -280,6 +321,32 @@ func (ls *LevelSlice) SizeSum() uint64 { return sum } +// NumVirtual returns the number of virtual sstables in the level. Its runtime is +// linear in the length of the slice. +func (ls *LevelSlice) NumVirtual() uint64 { + var n uint64 + iter := ls.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if f.Virtual { + n++ + } + } + return n +} + +// VirtualSizeSum returns the sum of the sizes of the virtual sstables in the +// level. +func (ls *LevelSlice) VirtualSizeSum() uint64 { + var sum uint64 + iter := ls.Iter() + for f := iter.First(); f != nil; f = iter.Next() { + if f.Virtual { + sum += f.Size + } + } + return sum +} + // Reslice constructs a new slice backed by the same underlying level, with // new start and end positions. Reslice invokes the provided function, passing // two LevelIterators: one positioned to i's inclusive start and one diff --git a/vendor/github.com/cockroachdb/pebble/internal/manifest/version.go b/vendor/github.com/cockroachdb/pebble/internal/manifest/version.go index 313fc67..0a3b774 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/manifest/version.go +++ b/vendor/github.com/cockroachdb/pebble/internal/manifest/version.go @@ -49,6 +49,9 @@ type TableStats struct { // The number of point and range deletion entries in the table. NumDeletions uint64 // NumRangeKeySets is the total number of range key sets in the table. + // + // NB: If there's a chance that the sstable contains any range key sets, + // then NumRangeKeySets must be > 0. NumRangeKeySets uint64 // Estimate of the total disk space that may be dropped by this table's // point deletions by compacting them. @@ -60,9 +63,11 @@ type TableStats struct { // overlapping data in L0 and ignores L0 sublevels, but the error that // introduces is expected to be small. // - // Tables in the bottommost level of the LSM may have a nonzero estimate - // if snapshots or move compactions prevented the elision of their range - // tombstones. + // Tables in the bottommost level of the LSM may have a nonzero estimate if + // snapshots or move compactions prevented the elision of their range + // tombstones. A table in the bottommost level that was ingested into L6 + // will have a zero estimate, because the file's sequence numbers indicate + // that the tombstone cannot drop any data contained within the file itself. RangeDeletionsBytesEstimate uint64 // Total size of value blocks and value index block. ValueBlocksSize uint64 @@ -119,43 +124,72 @@ func (s CompactionState) String() string { } } -// FileMetadata holds the metadata for an on-disk table. +// FileMetadata is maintained for leveled-ssts, i.e., they belong to a level of +// some version. FileMetadata does not contain the actual level of the sst, +// since such leveled-ssts can move across levels in different versions, while +// sharing the same FileMetadata. There are two kinds of leveled-ssts, physical +// and virtual. Underlying both leveled-ssts is a backing-sst, for which the +// only state is FileBacking. A backing-sst is level-less. It is possible for a +// backing-sst to be referred to by a physical sst in one version and by one or +// more virtual ssts in one or more versions. A backing-sst becomes obsolete +// and can be deleted once it is no longer required by any physical or virtual +// sst in any version. +// +// We maintain some invariants: +// +// 1. Each physical and virtual sst will have a unique FileMetadata.FileNum, +// and there will be exactly one FileMetadata associated with the FileNum. +// +// 2. Within a version, a backing-sst is either only referred to by one +// physical sst or one or more virtual ssts. +// +// 3. Once a backing-sst is referred to by a virtual sst in the latest version, +// it cannot go back to being referred to by a physical sst in any future +// version. +// +// Once a physical sst is no longer needed by any version, we will no longer +// maintain the file metadata associated with it. We will still maintain the +// FileBacking associated with the physical sst if the backing sst is required +// by any virtual ssts in any version. type FileMetadata struct { - // Atomic contains fields which are accessed atomically. Go allocations - // are guaranteed to be 64-bit aligned which we take advantage of by - // placing the 64-bit fields which we access atomically at the beginning - // of the FileMetadata struct. For more information, see - // https://golang.org/pkg/sync/atomic/#pkg-note-BUG. - Atomic struct { - // AllowedSeeks is used to determine if a file should be picked for - // a read triggered compaction. It is decremented when read sampling - // in pebble.Iterator after every after every positioning operation - // that returns a user key (eg. Next, Prev, SeekGE, SeekLT, etc). - AllowedSeeks int64 + // AllowedSeeks is used to determine if a file should be picked for + // a read triggered compaction. It is decremented when read sampling + // in pebble.Iterator after every after every positioning operation + // that returns a user key (eg. Next, Prev, SeekGE, SeekLT, etc). + AllowedSeeks atomic.Int64 - // statsValid is 1 if stats have been loaded for the table. The - // TableStats structure is populated only if valid is 1. - statsValid uint32 - } + // statsValid indicates if stats have been loaded for the table. The + // TableStats structure is populated only if valid is true. + statsValid atomic.Bool + + // FileBacking is the state which backs either a physical or virtual + // sstables. + FileBacking *FileBacking // InitAllowedSeeks is the inital value of allowed seeks. This is used // to re-set allowed seeks on a file once it hits 0. InitAllowedSeeks int64 - - // Reference count for the file: incremented when a file is added to a - // version and decremented when the version is unreferenced. The file is - // obsolete when the reference count falls to zero. - Refs int32 // FileNum is the file number. + // + // INVARIANT: when !FileMetadata.Virtual, FileNum == FileBacking.DiskFileNum. FileNum base.FileNum - // Size is the size of the file, in bytes. + // Size is the size of the file, in bytes. Size is an approximate value for + // virtual sstables. + // + // INVARIANTS: + // - When !FileMetadata.Virtual, Size == FileBacking.Size. + // - Size should be non-zero. Size 0 virtual sstables must not be created. Size uint64 // File creation time in seconds since the epoch (1970-01-01 00:00:00 // UTC). For ingested sstables, this corresponds to the time the file was - // ingested. + // ingested. For virtual sstables, this corresponds to the wall clock time + // when the FileMetadata for the virtual sstable was first created. CreationTime int64 - // Smallest and largest sequence numbers in the table, across both point and - // range keys. + // Lower and upper bounds for the smallest and largest sequence numbers in + // the table, across both point and range keys. For physical sstables, these + // values are tight bounds. For virtual sstables, there is no guarantee that + // there will be keys with SmallestSeqNum or LargestSeqNum within virtual + // sstable bounds. SmallestSeqNum uint64 LargestSeqNum uint64 // SmallestPointKey and LargestPointKey are the inclusive bounds for the @@ -178,8 +212,17 @@ type FileMetadata struct { Smallest InternalKey Largest InternalKey // Stats describe table statistics. Protected by DB.mu. + // + // For virtual sstables, set stats upon virtual sstable creation as + // asynchronous computation of stats is not currently supported. + // + // TODO(bananabrick): To support manifest replay for virtual sstables, we + // probably need to compute virtual sstable stats asynchronously. Otherwise, + // we'd have to write virtual sstable stats to the version edit. Stats TableStats + // For L0 files only. Protected by DB.mu. Used to generate L0 sublevels and + // pick L0 compactions. Only accurate for the most recent Version. SubLevel int L0Index int minIntervalIndex int @@ -188,9 +231,6 @@ type FileMetadata struct { // NB: the alignment of this struct is 8 bytes. We pack all the bools to // ensure an optimal packing. - // For L0 files only. Protected by DB.mu. Used to generate L0 sublevels and - // pick L0 compactions. Only accurate for the most recent Version. - // // IsIntraL0Compacting is set to True if this file is part of an intra-L0 // compaction. When it's true, IsCompacting must also return true. If // Compacting is true and IsIntraL0Compacting is false for an L0 file, the @@ -226,6 +266,212 @@ type FileMetadata struct { // key type (point or range) corresponds to the smallest and largest overall // table bounds. boundTypeSmallest, boundTypeLargest boundType + // Virtual is true if the FileMetadata belongs to a virtual sstable. + Virtual bool +} + +// PhysicalFileMeta is used by functions which want a guarantee that their input +// belongs to a physical sst and not a virtual sst. +// +// NB: This type should only be constructed by calling +// FileMetadata.PhysicalMeta. +type PhysicalFileMeta struct { + *FileMetadata +} + +// VirtualFileMeta is used by functions which want a guarantee that their input +// belongs to a virtual sst and not a physical sst. +// +// A VirtualFileMeta inherits all the same fields as a FileMetadata. These +// fields have additional invariants imposed on them, and/or slightly varying +// meanings: +// - Smallest and Largest (and their counterparts +// {Smallest, Largest}{Point,Range}Key) remain tight bounds that represent a +// key at that exact bound. We make the effort to determine the next smallest +// or largest key in an sstable after virtualizing it, to maintain this +// tightness. If the largest is a sentinel key (IsExclusiveSentinel()), it +// could mean that a rangedel or range key ends at that user key, or has been +// truncated to that user key. +// - One invariant is that if a rangedel or range key is truncated on its +// upper bound, the virtual sstable *must* have a rangedel or range key +// sentinel key as its upper bound. This is because truncation yields +// an exclusive upper bound for the rangedel/rangekey, and if there are +// any points at that exclusive upper bound within the same virtual +// sstable, those could get uncovered by this truncation. We enforce this +// invariant in calls to keyspan.Truncate. +// - Size is an estimate of the size of the virtualized portion of this sstable. +// The underlying file's size is stored in FileBacking.Size, though it could +// also be estimated or could correspond to just the referenced portion of +// a file (eg. if the file originated on another node). +// - Size must be > 0. +// - SmallestSeqNum and LargestSeqNum are loose bounds for virtual sstables. +// This means that all keys in the virtual sstable must have seqnums within +// [SmallestSeqNum, LargestSeqNum], however there's no guarantee that there's +// a key with a seqnum at either of the bounds. Calculating tight seqnum +// bounds would be too expensive and deliver little value. +// +// NB: This type should only be constructed by calling FileMetadata.VirtualMeta. +type VirtualFileMeta struct { + *FileMetadata +} + +// PhysicalMeta should be the only source of creating the PhysicalFileMeta +// wrapper type. +func (m *FileMetadata) PhysicalMeta() PhysicalFileMeta { + if m.Virtual { + panic("pebble: file metadata does not belong to a physical sstable") + } + return PhysicalFileMeta{ + m, + } +} + +// VirtualMeta should be the only source of creating the VirtualFileMeta wrapper +// type. +func (m *FileMetadata) VirtualMeta() VirtualFileMeta { + if !m.Virtual { + panic("pebble: file metadata does not belong to a virtual sstable") + } + return VirtualFileMeta{ + m, + } +} + +// FileBacking either backs a single physical sstable, or one or more virtual +// sstables. +// +// See the comment above the FileMetadata type for sstable terminology. +type FileBacking struct { + // Reference count for the backing file on disk: incremented when a + // physical or virtual sstable which is backed by the FileBacking is + // added to a version and decremented when the version is unreferenced. + // We ref count in order to determine when it is safe to delete a + // backing sst file from disk. The backing file is obsolete when the + // reference count falls to zero. + refs atomic.Int32 + // latestVersionRefs are the references to the FileBacking in the + // latest version. This reference can be through a single physical + // sstable in the latest version, or one or more virtual sstables in the + // latest version. + // + // INVARIANT: latestVersionRefs <= refs. + latestVersionRefs atomic.Int32 + // VirtualizedSize is set iff the backing sst is only referred to by + // virtual ssts in the latest version. VirtualizedSize is the sum of the + // virtual sstable sizes of all of the virtual sstables in the latest + // version which are backed by the physical sstable. When a virtual + // sstable is removed from the latest version, we will decrement the + // VirtualizedSize. During compaction picking, we'll compensate a + // virtual sstable file size by + // (FileBacking.Size - FileBacking.VirtualizedSize) / latestVersionRefs. + // The intuition is that if FileBacking.Size - FileBacking.VirtualizedSize + // is high, then the space amplification due to virtual sstables is + // high, and we should pick the virtual sstable with a higher priority. + // + // TODO(bananabrick): Compensate the virtual sstable file size using + // the VirtualizedSize during compaction picking and test. + VirtualizedSize atomic.Uint64 + DiskFileNum base.DiskFileNum + Size uint64 +} + +// InitPhysicalBacking allocates and sets the FileBacking which is required by a +// physical sstable FileMetadata. +// +// Ensure that the state required by FileBacking, such as the FileNum, is +// already set on the FileMetadata before InitPhysicalBacking is called. +// Calling InitPhysicalBacking only after the relevant state has been set in the +// FileMetadata is not necessary in tests which don't rely on FileBacking. +func (m *FileMetadata) InitPhysicalBacking() { + if m.Virtual { + panic("pebble: virtual sstables should use a pre-existing FileBacking") + } + if m.FileBacking == nil { + m.FileBacking = &FileBacking{Size: m.Size, DiskFileNum: m.FileNum.DiskFileNum()} + } +} + +// InitProviderBacking creates a new FileBacking for a file backed by +// an objstorage.Provider. +func (m *FileMetadata) InitProviderBacking(fileNum base.DiskFileNum) { + if !m.Virtual { + panic("pebble: provider-backed sstables must be virtual") + } + if m.FileBacking == nil { + m.FileBacking = &FileBacking{DiskFileNum: fileNum} + } +} + +// ValidateVirtual should be called once the FileMetadata for a virtual sstable +// is created to verify that the fields of the virtual sstable are sound. +func (m *FileMetadata) ValidateVirtual(createdFrom *FileMetadata) { + if !m.Virtual { + panic("pebble: invalid virtual sstable") + } + + if createdFrom.SmallestSeqNum != m.SmallestSeqNum { + panic("pebble: invalid smallest sequence number for virtual sstable") + } + + if createdFrom.LargestSeqNum != m.LargestSeqNum { + panic("pebble: invalid largest sequence number for virtual sstable") + } + + if createdFrom.FileBacking != nil && createdFrom.FileBacking != m.FileBacking { + panic("pebble: invalid physical sstable state for virtual sstable") + } + + if m.Size == 0 { + panic("pebble: virtual sstable size must be set upon creation") + } +} + +// Refs returns the refcount of backing sstable. +func (m *FileMetadata) Refs() int32 { + return m.FileBacking.refs.Load() +} + +// Ref increments the ref count associated with the backing sstable. +func (m *FileMetadata) Ref() { + m.FileBacking.refs.Add(1) +} + +// Unref decrements the ref count associated with the backing sstable. +func (m *FileMetadata) Unref() int32 { + v := m.FileBacking.refs.Add(-1) + if invariants.Enabled && v < 0 { + panic("pebble: invalid FileMetadata refcounting") + } + return v +} + +// LatestRef increments the latest ref count associated with the backing +// sstable. +func (m *FileMetadata) LatestRef() { + m.FileBacking.latestVersionRefs.Add(1) + + if m.Virtual { + m.FileBacking.VirtualizedSize.Add(m.Size) + } +} + +// LatestUnref decrements the latest ref count associated with the backing +// sstable. +func (m *FileMetadata) LatestUnref() int32 { + if m.Virtual { + m.FileBacking.VirtualizedSize.Add(-m.Size) + } + + v := m.FileBacking.latestVersionRefs.Add(-1) + if invariants.Enabled && v < 0 { + panic("pebble: invalid FileMetadata latest refcounting") + } + return v +} + +// LatestRefs returns the latest ref count associated with the backing sstable. +func (m *FileMetadata) LatestRefs() int32 { + return m.FileBacking.latestVersionRefs.Load() } // SetCompactionState transitions this file's compaction state to the given @@ -263,22 +509,14 @@ func (m *FileMetadata) IsCompacting() bool { // returns true, the Stats field may be read (with or without holding the // database mutex). func (m *FileMetadata) StatsValid() bool { - return atomic.LoadUint32(&m.Atomic.statsValid) == 1 -} - -// StatsValidLocked returns true if the table stats have been populated. -// StatsValidLocked requires DB.mu is held when it's invoked, and it avoids the -// overhead of an atomic load. This is possible because table stats validity is -// only set while DB.mu is held. -func (m *FileMetadata) StatsValidLocked() bool { - return m.Atomic.statsValid == 1 + return m.statsValid.Load() } // StatsMarkValid marks the TableStats as valid. The caller must hold DB.mu // while populating TableStats and calling StatsMarkValud. Once stats are // populated, they must not be mutated. func (m *FileMetadata) StatsMarkValid() { - atomic.StoreUint32(&m.Atomic.statsValid, 1) + m.statsValid.Store(true) } // ExtendPointKeyBounds attempts to extend the lower and upper point key bounds @@ -369,6 +607,13 @@ func (m *FileMetadata) Overlaps(cmp Compare, start []byte, end []byte, exclusive return true } +// ContainedWithinSpan returns true if the file key range completely overlaps with the +// given range ("end" is assumed to exclusive). +func (m *FileMetadata) ContainedWithinSpan(cmp Compare, start, end []byte) bool { + lowerCmp, upperCmp := cmp(m.Smallest.UserKey, start), cmp(m.Largest.UserKey, end) + return lowerCmp >= 0 && (upperCmp < 0 || (upperCmp == 0 && m.Largest.IsExclusiveSentinel())) +} + // ContainsKeyType returns whether or not the file contains keys of the provided // type. func (m *FileMetadata) ContainsKeyType(kt KeyType) bool { @@ -466,6 +711,7 @@ func (m *FileMetadata) DebugString(format base.FormatKey, verbose bool) string { if !verbose { return b.String() } + fmt.Fprintf(&b, " seqnums:[%d-%d]", m.SmallestSeqNum, m.LargestSeqNum) if m.HasPointKeys { fmt.Fprintf(&b, " points:[%s-%s]", m.SmallestPointKey.Pretty(format), m.LargestPointKey.Pretty(format)) @@ -479,9 +725,9 @@ func (m *FileMetadata) DebugString(format base.FormatKey, verbose bool) string { // ParseFileMetadataDebug parses a FileMetadata from its DebugString // representation. -func ParseFileMetadataDebug(s string) (m FileMetadata, err error) { +func ParseFileMetadataDebug(s string) (*FileMetadata, error) { // Split lines of the form: - // 000000:[a#0,SET-z#0,SET] points:[...] ranges:[...] + // 000000:[a#0,SET-z#0,SET] seqnums:[5-5] points:[...] ranges:[...] fields := strings.FieldsFunc(s, func(c rune) bool { switch c { case ':', '[', '-', ']': @@ -491,10 +737,24 @@ func ParseFileMetadataDebug(s string) (m FileMetadata, err error) { } }) if len(fields)%3 != 0 { - return m, errors.Newf("malformed input: %s", s) + return nil, errors.Newf("malformed input: %s", s) } + m := &FileMetadata{} for len(fields) > 0 { prefix := fields[0] + if prefix == "seqnums" { + smallestSeqNum, err := strconv.ParseUint(fields[1], 10, 64) + if err != nil { + return m, errors.Newf("malformed input: %s: %s", s, err) + } + largestSeqNum, err := strconv.ParseUint(fields[2], 10, 64) + if err != nil { + return m, errors.Newf("malformed input: %s: %s", s, err) + } + m.SmallestSeqNum, m.LargestSeqNum = smallestSeqNum, largestSeqNum + fields = fields[3:] + continue + } smallest := base.ParsePrettyInternalKey(fields[1]) largest := base.ParsePrettyInternalKey(fields[2]) switch prefix { @@ -522,7 +782,8 @@ func ParseFileMetadataDebug(s string) (m FileMetadata, err error) { m.SmallestPointKey, m.LargestPointKey = m.Smallest, m.Largest m.HasPointKeys = true } - return + m.InitPhysicalBacking() + return m, nil } // Validate validates the metadata for consistency with itself, returning an @@ -584,6 +845,11 @@ func (m *FileMetadata) Validate(cmp Compare, formatKey base.FormatKey) error { } } + // Ensure that FileMetadata.Init was called. + if m.FileBacking == nil { + return base.CorruptionErrorf("file metadata FileBacking not set") + } + return nil } @@ -756,6 +1022,9 @@ func NewVersion( } else { v.Levels[l].tree.cmp = btreeCmpSmallestKey(cmp) } + for _, f := range files[l] { + v.Levels[l].totalSize += f.Size + } } if err := v.InitL0Sublevels(cmp, formatKey, flushSplitBytes); err != nil { panic(err) @@ -788,7 +1057,7 @@ func NewVersion( // key in a higher level table that has both the same user key and a higher // sequence number. type Version struct { - refs int32 + refs atomic.Int32 // The level 0 sstables are organized in a series of sublevels. Similar to // the seqnum invariant in normal levels, there is no internal key in a @@ -805,7 +1074,7 @@ type Version struct { // usage. // // L0Sublevels.Levels contains L0 files ordered by sublevels. All the files - // in Files[0] are in L0Sublevels.Levels. L0SublevelFiles is also set to + // in Levels[0] are in L0Sublevels.Levels. L0SublevelFiles is also set to // a reference to that slice, as that slice is necessary for iterator // creation and needs to outlast L0Sublevels. L0Sublevels *L0Sublevels @@ -820,7 +1089,7 @@ type Version struct { // The callback to invoke when the last reference to a version is // removed. Will be called with list.mu held. - Deleted func(obsolete []*FileMetadata) + Deleted func(obsolete []*FileBacking) // Stats holds aggregated stats about the version maintained from // version to version. @@ -849,15 +1118,21 @@ func (v *Version) DebugString(format base.FormatKey) string { return v.string(format, true) } +func describeSublevels(format base.FormatKey, verbose bool, sublevels []LevelSlice) string { + var buf bytes.Buffer + for sublevel := len(sublevels) - 1; sublevel >= 0; sublevel-- { + fmt.Fprintf(&buf, "0.%d:\n", sublevel) + sublevels[sublevel].Each(func(f *FileMetadata) { + fmt.Fprintf(&buf, " %s\n", f.DebugString(format, verbose)) + }) + } + return buf.String() +} + func (v *Version) string(format base.FormatKey, verbose bool) string { var buf bytes.Buffer if len(v.L0SublevelFiles) > 0 { - for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- { - fmt.Fprintf(&buf, "0.%d:\n", sublevel) - v.L0SublevelFiles[sublevel].Each(func(f *FileMetadata) { - fmt.Fprintf(&buf, " %s\n", f.DebugString(format, verbose)) - }) - } + fmt.Fprintf(&buf, "%s", describeSublevels(format, verbose, v.L0SublevelFiles)) } for level := 1; level < NumLevels; level++ { if v.Levels[level].Empty() { @@ -898,7 +1173,7 @@ func ParseVersionDebug( m.SmallestPointKey, m.LargestPointKey = m.Smallest, m.Largest m.HasPointKeys = true } - files[level] = append(files[level], &m) + files[level] = append(files[level], m) } } // Reverse the order of L0 files. This ensures we construct the same @@ -912,12 +1187,12 @@ func ParseVersionDebug( // Refs returns the number of references to the version. func (v *Version) Refs() int32 { - return atomic.LoadInt32(&v.refs) + return v.refs.Load() } // Ref increments the version refcount. func (v *Version) Ref() { - atomic.AddInt32(&v.refs, 1) + v.refs.Add(1) } // Unref decrements the version refcount. If the last reference to the version @@ -925,12 +1200,11 @@ func (v *Version) Ref() { // Deleted callback is invoked. Requires that the VersionList mutex is NOT // locked. func (v *Version) Unref() { - if atomic.AddInt32(&v.refs, -1) == 0 { - obsolete := v.unrefFiles() + if v.refs.Add(-1) == 0 { l := v.list l.mu.Lock() l.Remove(v) - v.Deleted(obsolete) + v.Deleted(v.unrefFiles()) l.mu.Unlock() } } @@ -940,14 +1214,14 @@ func (v *Version) Unref() { // the Deleted callback is invoked. Requires that the VersionList mutex is // already locked. func (v *Version) UnrefLocked() { - if atomic.AddInt32(&v.refs, -1) == 0 { + if v.refs.Add(-1) == 0 { v.list.Remove(v) v.Deleted(v.unrefFiles()) } } -func (v *Version) unrefFiles() []*FileMetadata { - var obsolete []*FileMetadata +func (v *Version) unrefFiles() []*FileBacking { + var obsolete []*FileBacking for _, lm := range v.Levels { obsolete = append(obsolete, lm.release()...) } @@ -1079,16 +1353,20 @@ func (v *Version) Overlaps( // CheckOrdering checks that the files are consistent with respect to // increasing file numbers (for level 0 files) and increasing and non- // overlapping internal key ranges (for level non-0 files). -func (v *Version) CheckOrdering(cmp Compare, format base.FormatKey) error { +func (v *Version) CheckOrdering( + cmp Compare, format base.FormatKey, order OrderingInvariants, +) error { for sublevel := len(v.L0SublevelFiles) - 1; sublevel >= 0; sublevel-- { sublevelIter := v.L0SublevelFiles[sublevel].Iter() - if err := CheckOrdering(cmp, format, L0Sublevel(sublevel), sublevelIter); err != nil { + // Sublevels have NEVER allowed split user keys, so we can pass + // ProhibitSplitUserKeys. + if err := CheckOrdering(cmp, format, L0Sublevel(sublevel), sublevelIter, ProhibitSplitUserKeys); err != nil { return base.CorruptionErrorf("%s\n%s", err, v.DebugString(format)) } } for level, lm := range v.Levels { - if err := CheckOrdering(cmp, format, Level(level), lm.Iter()); err != nil { + if err := CheckOrdering(cmp, format, Level(level), lm.Iter(), order); err != nil { return base.CorruptionErrorf("%s\n%s", err, v.DebugString(format)) } } @@ -1157,10 +1435,34 @@ func (l *VersionList) Remove(v *Version) { v.list = nil // avoid memory leaks } +// OrderingInvariants dictates the file ordering invariants active. +type OrderingInvariants int8 + +const ( + // ProhibitSplitUserKeys indicates that adjacent files within a level cannot + // contain the same user key. + ProhibitSplitUserKeys OrderingInvariants = iota + // AllowSplitUserKeys indicates that adjacent files within a level may + // contain the same user key. This is only allowed by historical format + // major versions. + // + // TODO(jackson): Remove. + AllowSplitUserKeys +) + // CheckOrdering checks that the files are consistent with respect to // seqnums (for level 0 files -- see detailed comment below) and increasing and non- // overlapping internal key ranges (for non-level 0 files). -func CheckOrdering(cmp Compare, format base.FormatKey, level Level, files LevelIterator) error { +// +// The ordering field may be passed AllowSplitUserKeys to allow adjacent files that are both +// inclusive of the same user key. Pebble no longer creates version edits +// installing such files, and Pebble databases with sufficiently high format +// major version should no longer have any such files within their LSM. +// TODO(jackson): Remove AllowSplitUserKeys when we remove support for the +// earlier format major versions. +func CheckOrdering( + cmp Compare, format base.FormatKey, level Level, files LevelIterator, ordering OrderingInvariants, +) error { // The invariants to check for L0 sublevels are the same as the ones to // check for all other levels. However, if L0 is not organized into // sublevels, or if all L0 files are being passed in, we do the legacy L0 @@ -1238,11 +1540,29 @@ func CheckOrdering(cmp Compare, format base.FormatKey, level Level, files LevelI prev.Smallest.Pretty(format), prev.Largest.Pretty(format), f.Smallest.Pretty(format), f.Largest.Pretty(format)) } - if base.InternalCompare(cmp, prev.Largest, f.Smallest) >= 0 { - return base.CorruptionErrorf("%s files %s and %s have overlapping ranges: [%s-%s] vs [%s-%s]", - errors.Safe(level), errors.Safe(prev.FileNum), errors.Safe(f.FileNum), - prev.Smallest.Pretty(format), prev.Largest.Pretty(format), - f.Smallest.Pretty(format), f.Largest.Pretty(format)) + + // What's considered "overlapping" is dependent on the format + // major version. If ordering=ProhibitSplitUserKeys, then both + // files cannot contain keys with the same user keys. If the + // bounds have the same user key, the previous file's boundary + // must have a Trailer indicating that it's exclusive. + switch ordering { + case AllowSplitUserKeys: + if base.InternalCompare(cmp, prev.Largest, f.Smallest) >= 0 { + return base.CorruptionErrorf("%s files %s and %s have overlapping ranges: [%s-%s] vs [%s-%s]", + errors.Safe(level), errors.Safe(prev.FileNum), errors.Safe(f.FileNum), + prev.Smallest.Pretty(format), prev.Largest.Pretty(format), + f.Smallest.Pretty(format), f.Largest.Pretty(format)) + } + case ProhibitSplitUserKeys: + if v := cmp(prev.Largest.UserKey, f.Smallest.UserKey); v > 0 || (v == 0 && !prev.Largest.IsExclusiveSentinel()) { + return base.CorruptionErrorf("%s files %s and %s have overlapping ranges: [%s-%s] vs [%s-%s]", + errors.Safe(level), errors.Safe(prev.FileNum), errors.Safe(f.FileNum), + prev.Smallest.Pretty(format), prev.Largest.Pretty(format), + f.Smallest.Pretty(format), f.Largest.Pretty(format)) + } + default: + panic("unreachable") } } } diff --git a/vendor/github.com/cockroachdb/pebble/internal/manifest/version_edit.go b/vendor/github.com/cockroachdb/pebble/internal/manifest/version_edit.go index ed591f8..d7853c2 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/manifest/version_edit.go +++ b/vendor/github.com/cockroachdb/pebble/internal/manifest/version_edit.go @@ -11,11 +11,11 @@ import ( "fmt" "io" "sort" - "sync/atomic" "time" "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" ) // TODO(peter): describe the MANIFEST file format, independently of the C++ @@ -51,7 +51,9 @@ const ( tagMaxColumnFamily = 203 // Pebble tags. - tagNewFile5 = 104 // Range keys. + tagNewFile5 = 104 // Range keys. + tagCreatedBackingTable = 105 + tagRemovedBackingTable = 106 // The custom tags sub-format used by tagNewFile4 and above. customTagTerminate = 1 @@ -59,6 +61,7 @@ const ( customTagCreationTime = 6 customTagPathID = 65 customTagNonSafeIgnoreMask = 1 << 6 + customTagVirtual = 66 ) // DeletedFileEntry holds the state for a file deletion from a level. The file @@ -73,6 +76,9 @@ type DeletedFileEntry struct { type NewFileEntry struct { Level int Meta *FileMetadata + // BackingFileNum is only set during manifest replay, and only for virtual + // sstables. + BackingFileNum base.DiskFileNum } // VersionEdit holds the state for an edit to a Version along with other @@ -110,9 +116,42 @@ type VersionEdit struct { // found that there was no overlapping file at the higher level). DeletedFiles map[DeletedFileEntry]*FileMetadata NewFiles []NewFileEntry + // CreatedBackingTables can be used to preserve the FileBacking associated + // with a physical sstable. This is useful when virtual sstables in the + // latest version are reconstructed during manifest replay, and we also need + // to reconstruct the FileBacking which is required by these virtual + // sstables. + // + // INVARIANT: The FileBacking associated with a physical sstable must only + // be added as a backing file in the same version edit where the physical + // sstable is first virtualized. This means that the physical sstable must + // be present in DeletedFiles and that there must be at least one virtual + // sstable with the same FileBacking as the physical sstable in NewFiles. A + // file must be present in CreatedBackingTables in exactly one version edit. + // The physical sstable associated with the FileBacking must also not be + // present in NewFiles. + CreatedBackingTables []*FileBacking + // RemovedBackingTables is used to remove the FileBacking associated with a + // virtual sstable. Note that a backing sstable can be removed as soon as + // there are no virtual sstables in the latest version which are using the + // backing sstable, but the backing sstable doesn't necessarily have to be + // removed atomically with the version edit which removes the last virtual + // sstable associated with the backing sstable. The removal can happen in a + // future version edit. + // + // INVARIANT: A file must only be added to RemovedBackingTables if it was + // added to CreateBackingTables in a prior version edit. The same version + // edit also cannot have the same file present in both CreateBackingTables + // and RemovedBackingTables. A file must be present in RemovedBackingTables + // in exactly one version edit. + RemovedBackingTables []base.DiskFileNum } // Decode decodes an edit from the specified reader. +// +// Note that the Decode step will not set the FileBacking for virtual sstables +// and the responsibility is left to the caller. However, the Decode step will +// populate the NewFileEntry.BackingFileNum in VersionEdit.NewFiles. func (v *VersionEdit) Decode(r io.Reader) error { br, ok := r.(byteReader) if !ok { @@ -165,6 +204,28 @@ func (v *VersionEdit) Decode(r io.Reader) error { } // NB: RocksDB does not use compaction pointers anymore. + case tagRemovedBackingTable: + n, err := d.readUvarint() + if err != nil { + return err + } + v.RemovedBackingTables = append( + v.RemovedBackingTables, base.FileNum(n).DiskFileNum(), + ) + case tagCreatedBackingTable: + dfn, err := d.readUvarint() + if err != nil { + return err + } + size, err := d.readUvarint() + if err != nil { + return err + } + fileBacking := &FileBacking{ + DiskFileNum: base.FileNum(dfn).DiskFileNum(), + Size: size, + } + v.CreatedBackingTables = append(v.CreatedBackingTables, fileBacking) case tagDeletedFile: level, err := d.readLevel() if err != nil { @@ -270,6 +331,10 @@ func (v *VersionEdit) Decode(r io.Reader) error { } var markedForCompaction bool var creationTime uint64 + virtualState := struct { + virtual bool + backingFileNum uint64 + }{} if tag == tagNewFile4 || tag == tagNewFile5 { for { customTag, err := d.readUvarint() @@ -278,7 +343,16 @@ func (v *VersionEdit) Decode(r io.Reader) error { } if customTag == customTagTerminate { break + } else if customTag == customTagVirtual { + virtualState.virtual = true + n, err := d.readUvarint() + if err != nil { + return err + } + virtualState.backingFileNum = n + continue } + field, err := d.readBytes() if err != nil { return err @@ -314,6 +388,7 @@ func (v *VersionEdit) Decode(r io.Reader) error { SmallestSeqNum: smallestSeqNum, LargestSeqNum: largestSeqNum, MarkedForCompaction: markedForCompaction, + Virtual: virtualState.virtual, } if tag != tagNewFile5 { // no range keys present m.SmallestPointKey = base.DecodeInternalKey(smallestPointKey) @@ -345,10 +420,18 @@ func (v *VersionEdit) Decode(r io.Reader) error { } } m.boundsSet = true - v.NewFiles = append(v.NewFiles, NewFileEntry{ + if !virtualState.virtual { + m.InitPhysicalBacking() + } + + nfe := NewFileEntry{ Level: level, Meta: m, - }) + } + if virtualState.virtual { + nfe.BackingFileNum = base.FileNum(virtualState.backingFileNum).DiskFileNum() + } + v.NewFiles = append(v.NewFiles, nfe) case tagPrevLogNumber: n, err := d.readUvarint() @@ -367,8 +450,7 @@ func (v *VersionEdit) Decode(r io.Reader) error { return nil } -// String implements fmt.Stringer for a VersionEdit. -func (v *VersionEdit) String() string { +func (v *VersionEdit) string(verbose bool, fmtKey base.FormatKey) string { var buf bytes.Buffer if v.ComparerName != "" { fmt.Fprintf(&buf, " comparer: %s", v.ComparerName) @@ -399,7 +481,12 @@ func (v *VersionEdit) String() string { fmt.Fprintf(&buf, " deleted: L%d %s\n", df.Level, df.FileNum) } for _, nf := range v.NewFiles { - fmt.Fprintf(&buf, " added: L%d %s", nf.Level, nf.Meta.String()) + fmt.Fprintf(&buf, " added: L%d", nf.Level) + if verbose { + fmt.Fprintf(&buf, " %s", nf.Meta.DebugString(fmtKey, true /* verbose */)) + } else { + fmt.Fprintf(&buf, " %s", nf.Meta.String()) + } if nf.Meta.CreationTime != 0 { fmt.Fprintf(&buf, " (%s)", time.Unix(nf.Meta.CreationTime, 0).UTC().Format(time.RFC3339)) @@ -409,6 +496,16 @@ func (v *VersionEdit) String() string { return buf.String() } +// DebugString is a more verbose version of String(). Use this in tests. +func (v *VersionEdit) DebugString(fmtKey base.FormatKey) string { + return v.string(true /* verbose */, fmtKey) +} + +// String implements fmt.Stringer for a VersionEdit. +func (v *VersionEdit) String() string { + return v.string(false /* verbose */, base.DefaultFormatter) +} + // Encode encodes an edit to the specified writer. func (v *VersionEdit) Encode(w io.Writer) error { e := versionEditEncoder{new(bytes.Buffer)} @@ -429,6 +526,15 @@ func (v *VersionEdit) Encode(w io.Writer) error { e.writeUvarint(tagNextFileNumber) e.writeUvarint(uint64(v.NextFileNum)) } + for _, dfn := range v.RemovedBackingTables { + e.writeUvarint(tagRemovedBackingTable) + e.writeUvarint(uint64(dfn.FileNum())) + } + for _, fileBacking := range v.CreatedBackingTables { + e.writeUvarint(tagCreatedBackingTable) + e.writeUvarint(uint64(fileBacking.DiskFileNum.FileNum())) + e.writeUvarint(fileBacking.Size) + } // RocksDB requires LastSeqNum to be encoded for the first MANIFEST entry, // even though its value is zero. We detect this by encoding LastSeqNum when // ComparerName is set. @@ -442,7 +548,7 @@ func (v *VersionEdit) Encode(w io.Writer) error { e.writeUvarint(uint64(x.FileNum)) } for _, x := range v.NewFiles { - customFields := x.Meta.MarkedForCompaction || x.Meta.CreationTime != 0 + customFields := x.Meta.MarkedForCompaction || x.Meta.CreationTime != 0 || x.Meta.Virtual var tag uint64 switch { case x.Meta.HasRangeKeys: @@ -495,6 +601,10 @@ func (v *VersionEdit) Encode(w io.Writer) error { e.writeUvarint(customTagNeedsCompaction) e.writeBytes([]byte{1}) } + if x.Meta.Virtual { + e.writeUvarint(customTagVirtual) + e.writeUvarint(uint64(x.Meta.FileBacking.DiskFileNum.FileNum())) + } e.writeUvarint(customTagTerminate) } } @@ -502,6 +612,7 @@ func (v *VersionEdit) Encode(w io.Writer) error { return err } +// versionEditDecoder should be used to decode version edits. type versionEditDecoder struct { byteReader } @@ -581,10 +692,30 @@ func (e versionEditEncoder) writeUvarint(u uint64) { // BulkVersionEdit summarizes the files added and deleted from a set of version // edits. +// +// INVARIANTS: +// No file can be added to a level more than once. This is true globally, and +// also true for all of the calls to Accumulate for a single bulk version edit. +// +// No file can be removed from a level more than once. This is true globally, +// and also true for all of the calls to Accumulate for a single bulk version +// edit. +// +// A file must not be added and removed from a given level in the same version +// edit. +// +// A file that is being removed from a level must have been added to that level +// before (in a prior version edit). Note that a given file can be deleted from +// a level and added to another level in a single version edit type BulkVersionEdit struct { - Added [NumLevels][]*FileMetadata + Added [NumLevels]map[base.FileNum]*FileMetadata Deleted [NumLevels]map[base.FileNum]*FileMetadata + // AddedFileBacking is a map to support lookup so that we can populate the + // FileBacking of virtual sstables during manifest replay. + AddedFileBacking map[base.DiskFileNum]*FileBacking + RemovedFileBacking []base.DiskFileNum + // AddedByFileNum maps file number to file metadata for all added files // from accumulated version edits. AddedByFileNum is only populated if set // to non-nil by a caller. It must be set to non-nil when replaying @@ -603,6 +734,19 @@ type BulkVersionEdit struct { // Accumulate adds the file addition and deletions in the specified version // edit to the bulk edit's internal state. +// +// INVARIANTS: +// If a file is added to a given level in a call to Accumulate and then removed +// from that level in a subsequent call, the file will not be present in the +// resulting BulkVersionEdit.Deleted for that level. +// +// After accumulation of version edits, the bulk version edit may have +// information about a file which has been deleted from a level, but it may +// not have information about the same file added to the same level. The add +// could've occurred as part of a previous bulk version edit. In this case, +// the deleted file must be present in BulkVersionEdit.Deleted, at the end +// of the accumulation, because we need to decrease the refcount of the +// deleted file in Apply. func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error { for df, m := range ve.DeletedFiles { dmap := b.Deleted[df.Level] @@ -624,7 +768,28 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error { if m.MarkedForCompaction { b.MarkedForCompactionCountDiff-- } - dmap[df.FileNum] = m + if _, ok := b.Added[df.Level][df.FileNum]; !ok { + dmap[df.FileNum] = m + } else { + // Present in b.Added for the same level. + delete(b.Added[df.Level], df.FileNum) + } + } + + // Generate state for Added backing files. Note that these must be generated + // before we loop through the NewFiles, because we need to populate the + // FileBackings which might be used by the NewFiles loop. + if b.AddedFileBacking == nil { + b.AddedFileBacking = make(map[base.DiskFileNum]*FileBacking) + } + for _, fb := range ve.CreatedBackingTables { + if _, ok := b.AddedFileBacking[fb.DiskFileNum]; ok { + // There is already a FileBacking associated with fb.DiskFileNum. + // This should never happen. There must always be only one FileBacking + // associated with a backing sstable. + panic(fmt.Sprintf("pebble: duplicate file backing %s", fb.DiskFileNum.String())) + } + b.AddedFileBacking[fb.DiskFileNum] = fb } for _, nf := range ve.NewFiles { @@ -635,7 +800,21 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error { return base.CorruptionErrorf("pebble: file deleted L%d.%s before it was inserted", nf.Level, nf.Meta.FileNum) } } - b.Added[nf.Level] = append(b.Added[nf.Level], nf.Meta) + if nf.Meta.Virtual && nf.Meta.FileBacking == nil { + // FileBacking for a virtual sstable must only be nil if we're performing + // manifest replay. + nf.Meta.FileBacking = b.AddedFileBacking[nf.BackingFileNum] + if nf.Meta.FileBacking == nil { + return errors.Errorf("FileBacking for virtual sstable must not be nil") + } + } else if nf.Meta.FileBacking == nil { + return errors.Errorf("Added file L%d.%s's has no FileBacking", nf.Level, nf.Meta.FileNum) + } + + if b.Added[nf.Level] == nil { + b.Added[nf.Level] = make(map[base.FileNum]*FileMetadata) + } + b.Added[nf.Level][nf.Meta.FileNum] = nf.Meta if b.AddedByFileNum != nil { b.AddedByFileNum[nf.Meta.FileNum] = nf.Meta } @@ -643,36 +822,101 @@ func (b *BulkVersionEdit) Accumulate(ve *VersionEdit) error { b.MarkedForCompactionCountDiff++ } } + + // Since a file can be removed from backing files in exactly one version + // edit it is safe to just append without any de-duplication. + b.RemovedFileBacking = append(b.RemovedFileBacking, ve.RemovedBackingTables...) + return nil } +// AccumulateIncompleteAndApplySingleVE should be called if a single version edit +// is to be applied to the provided curr Version and if the caller needs to +// update the versionSet.zombieTables map. This function exists separately from +// BulkVersionEdit.Apply because it is easier to reason about properties +// regarding BulkVersionedit.Accumulate/Apply and zombie table generation, if we +// know that exactly one version edit is being accumulated. +// +// Note that the version edit passed into this function may be incomplete +// because compactions don't have the ref counting information necessary to +// populate VersionEdit.RemovedBackingTables. This function will complete such a +// version edit by populating RemovedBackingTables. +// +// Invariant: Any file being deleted through ve must belong to the curr Version. +// We can't have a delete for some arbitrary file which does not exist in curr. +func AccumulateIncompleteAndApplySingleVE( + ve *VersionEdit, + curr *Version, + cmp Compare, + formatKey base.FormatKey, + flushSplitBytes int64, + readCompactionRate int64, + backingStateMap map[base.DiskFileNum]*FileBacking, + addBackingFunc func(*FileBacking), + removeBackingFunc func(base.DiskFileNum), + orderingInvariants OrderingInvariants, +) (_ *Version, zombies map[base.DiskFileNum]uint64, _ error) { + if len(ve.RemovedBackingTables) != 0 { + panic("pebble: invalid incomplete version edit") + } + var b BulkVersionEdit + err := b.Accumulate(ve) + if err != nil { + return nil, nil, err + } + zombies = make(map[base.DiskFileNum]uint64) + v, err := b.Apply( + curr, cmp, formatKey, flushSplitBytes, readCompactionRate, zombies, orderingInvariants, + ) + if err != nil { + return nil, nil, err + } + + for _, s := range b.AddedFileBacking { + addBackingFunc(s) + } + + for fileNum := range zombies { + if _, ok := backingStateMap[fileNum]; ok { + // This table was backing some virtual sstable in the latest version, + // but is now a zombie. We add RemovedBackingTables entries for + // these, before the version edit is written to disk. + ve.RemovedBackingTables = append( + ve.RemovedBackingTables, fileNum, + ) + removeBackingFunc(fileNum) + } + } + return v, zombies, nil +} + // Apply applies the delta b to the current version to produce a new // version. The new version is consistent with respect to the comparer cmp. // // curr may be nil, which is equivalent to a pointer to a zero version. // -// On success, a map of zombie files containing the file numbers and sizes of -// deleted files is returned. These files are considered zombies because they -// are no longer referenced by the returned Version, but cannot be deleted from -// disk as they are still in use by the incoming Version. +// On success, if a non-nil zombies map is provided to Apply, the map is updated +// with file numbers and files sizes of deleted files. These files are +// considered zombies because they are no longer referenced by the returned +// Version, but cannot be deleted from disk as they are still in use by the +// incoming Version. func (b *BulkVersionEdit) Apply( curr *Version, cmp Compare, formatKey base.FormatKey, flushSplitBytes int64, readCompactionRate int64, -) (_ *Version, zombies map[base.FileNum]uint64, _ error) { - addZombie := func(fileNum base.FileNum, size uint64) { - if zombies == nil { - zombies = make(map[base.FileNum]uint64) - } - zombies[fileNum] = size - } - // The remove zombie function is used to handle tables that are moved from - // one level to another during a version edit (i.e. a "move" compaction). - removeZombie := func(fileNum base.FileNum) { + zombies map[base.DiskFileNum]uint64, + orderingInvariants OrderingInvariants, +) (*Version, error) { + addZombie := func(state *FileBacking) { if zombies != nil { - delete(zombies, fileNum) + zombies[state.DiskFileNum] = state.Size + } + } + removeZombie := func(state *FileBacking) { + if zombies != nil { + delete(zombies, state.DiskFileNum) } } @@ -684,7 +928,7 @@ func (b *BulkVersionEdit) Apply( } v.Stats.MarkedForCompaction += b.MarkedForCompactionCountDiff if v.Stats.MarkedForCompaction < 0 { - return nil, nil, base.CorruptionErrorf("pebble: version marked for compaction count negative") + return nil, base.CorruptionErrorf("pebble: version marked for compaction count negative") } for level := range v.Levels { @@ -705,7 +949,7 @@ func (b *BulkVersionEdit) Apply( // Initialize L0Sublevels. if curr == nil || curr.L0Sublevels == nil { if err := v.InitL0Sublevels(cmp, formatKey, flushSplitBytes); err != nil { - return nil, nil, errors.Wrap(err, "pebble: internal error") + return nil, errors.Wrap(err, "pebble: internal error") } } else { v.L0Sublevels = curr.L0Sublevels @@ -718,47 +962,70 @@ func (b *BulkVersionEdit) Apply( // Some edits on this level. lm := &v.Levels[level] lmRange := &v.RangeKeyLevels[level] - addedFiles := b.Added[level] - deletedMap := b.Deleted[level] - if n := v.Levels[level].Len() + len(addedFiles); n == 0 { - return nil, nil, base.CorruptionErrorf( + + addedFilesMap := b.Added[level] + deletedFilesMap := b.Deleted[level] + if n := v.Levels[level].Len() + len(addedFilesMap); n == 0 { + return nil, base.CorruptionErrorf( "pebble: internal error: No current or added files but have deleted files: %d", - errors.Safe(len(deletedMap))) + errors.Safe(len(deletedFilesMap))) } - // NB: addedFiles may be empty and it also is not necessarily - // internally consistent: it does not reflect deletions in deletedMap. + // NB: addedFilesMap may be empty. If a file is present in addedFilesMap + // for a level, it won't be present in deletedFilesMap for the same + // level. - for _, f := range deletedMap { - addZombie(f.FileNum, f.Size) - if obsolete := v.Levels[level].tree.Delete(f); obsolete { + for _, f := range deletedFilesMap { + if obsolete := v.Levels[level].remove(f); obsolete { // Deleting a file from the B-Tree may decrement its // reference count. However, because we cloned the // previous level's B-Tree, this should never result in a // file's reference count dropping to zero. err := errors.Errorf("pebble: internal error: file L%d.%s obsolete during B-Tree removal", level, f.FileNum) - return nil, nil, err + return nil, err } if f.HasRangeKeys { - if obsolete := v.RangeKeyLevels[level].tree.Delete(f); obsolete { + if obsolete := v.RangeKeyLevels[level].remove(f); obsolete { // Deleting a file from the B-Tree may decrement its // reference count. However, because we cloned the // previous level's B-Tree, this should never result in a // file's reference count dropping to zero. err := errors.Errorf("pebble: internal error: file L%d.%s obsolete during range-key B-Tree removal", level, f.FileNum) - return nil, nil, err + return nil, err } } + + // Note that a backing sst will only become a zombie if the + // references to it in the latest version is 0. We will remove the + // backing sst from the zombie list in the next loop if one of the + // addedFiles in any of the levels is referencing the backing sst. + // This is possible if a physical sstable is virtualized, or if it + // is moved. + latestRefCount := f.LatestRefs() + if latestRefCount <= 0 { + // If a file is present in deletedFilesMap for a level, then it + // must have already been added to the level previously, which + // means that its latest ref count cannot be 0. + err := errors.Errorf("pebble: internal error: incorrect latestRefs reference counting for file", f.FileNum) + return nil, err + } else if f.LatestUnref() == 0 { + addZombie(f.FileBacking) + } } + addedFiles := make([]*FileMetadata, 0, len(addedFilesMap)) + for _, f := range addedFilesMap { + addedFiles = append(addedFiles, f) + } + // Sort addedFiles by file number. This isn't necessary, but tests which + // replay invalid manifests check the error output, and the error output + // depends on the order in which files are added to the btree. + sort.Slice(addedFiles, func(i, j int) bool { + return addedFiles[i].FileNum < addedFiles[j].FileNum + }) + var sm, la *FileMetadata for _, f := range addedFiles { - if _, ok := deletedMap[f.FileNum]; ok { - // Already called addZombie on this file in the preceding - // loop, so we don't need to do it here. - continue - } - // NB: allowedSeeks is used for read triggered compactions. It is set using // Options.Experimental.ReadCompactionRate which defaults to 32KB. var allowedSeeks int64 @@ -768,20 +1035,23 @@ func (b *BulkVersionEdit) Apply( if allowedSeeks < 100 { allowedSeeks = 100 } - atomic.StoreInt64(&f.Atomic.AllowedSeeks, allowedSeeks) + f.AllowedSeeks.Store(allowedSeeks) f.InitAllowedSeeks = allowedSeeks - err := lm.tree.Insert(f) + err := lm.insert(f) + // We're adding this file to the new version, so increment the + // latest refs count. + f.LatestRef() if err != nil { - return nil, nil, errors.Wrap(err, "pebble") + return nil, errors.Wrap(err, "pebble") } if f.HasRangeKeys { - err = lmRange.tree.Insert(f) + err = lmRange.insert(f) if err != nil { - return nil, nil, errors.Wrap(err, "pebble") + return nil, errors.Wrap(err, "pebble") } } - removeZombie(f.FileNum) + removeZombie(f.FileBacking) // Track the keys with the smallest and largest keys, so that we can // check consistency of the modified span. if sm == nil || base.InternalCompare(cmp, sm.Smallest, f.Smallest) > 0 { @@ -793,27 +1063,36 @@ func (b *BulkVersionEdit) Apply( } if level == 0 { - if curr != nil && curr.L0Sublevels != nil && len(deletedMap) == 0 { + if curr != nil && curr.L0Sublevels != nil && len(deletedFilesMap) == 0 { // Flushes and ingestions that do not delete any L0 files do not require // a regeneration of L0Sublevels from scratch. We can instead generate // it incrementally. var err error // AddL0Files requires addedFiles to be sorted in seqnum order. - addedFiles = append([]*FileMetadata(nil), addedFiles...) SortBySeqNum(addedFiles) v.L0Sublevels, err = curr.L0Sublevels.AddL0Files(addedFiles, flushSplitBytes, &v.Levels[0]) if errors.Is(err, errInvalidL0SublevelsOpt) { err = v.InitL0Sublevels(cmp, formatKey, flushSplitBytes) + } else if invariants.Enabled && err == nil { + copyOfSublevels, err := NewL0Sublevels(&v.Levels[0], cmp, formatKey, flushSplitBytes) + if err != nil { + panic(fmt.Sprintf("error when regenerating sublevels: %s", err)) + } + s1 := describeSublevels(base.DefaultFormatter, false /* verbose */, copyOfSublevels.Levels) + s2 := describeSublevels(base.DefaultFormatter, false /* verbose */, v.L0Sublevels.Levels) + if s1 != s2 { + panic(fmt.Sprintf("incremental L0 sublevel generation produced different output than regeneration: %s != %s", s1, s2)) + } } if err != nil { - return nil, nil, errors.Wrap(err, "pebble: internal error") + return nil, errors.Wrap(err, "pebble: internal error") } v.L0SublevelFiles = v.L0Sublevels.Levels } else if err := v.InitL0Sublevels(cmp, formatKey, flushSplitBytes); err != nil { - return nil, nil, errors.Wrap(err, "pebble: internal error") + return nil, errors.Wrap(err, "pebble: internal error") } - if err := CheckOrdering(cmp, formatKey, Level(0), v.Levels[level].Iter()); err != nil { - return nil, nil, errors.Wrap(err, "pebble: internal error") + if err := CheckOrdering(cmp, formatKey, Level(0), v.Levels[level].Iter(), orderingInvariants); err != nil { + return nil, errors.Wrap(err, "pebble: internal error") } continue } @@ -833,10 +1112,10 @@ func (b *BulkVersionEdit) Apply( end.Prev() } }) - if err := CheckOrdering(cmp, formatKey, Level(level), check.Iter()); err != nil { - return nil, nil, errors.Wrap(err, "pebble: internal error") + if err := CheckOrdering(cmp, formatKey, Level(level), check.Iter(), orderingInvariants); err != nil { + return nil, errors.Wrap(err, "pebble: internal error") } } } - return v, zombies, nil + return v, nil } diff --git a/vendor/github.com/cockroachdb/pebble/internal/private/sstable.go b/vendor/github.com/cockroachdb/pebble/internal/private/sstable.go index fb38139..780dd56 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/private/sstable.go +++ b/vendor/github.com/cockroachdb/pebble/internal/private/sstable.go @@ -8,7 +8,7 @@ import "github.com/cockroachdb/pebble/internal/base" // SSTableCacheOpts is a hook for specifying cache options to // sstable.NewReader. -var SSTableCacheOpts func(cacheID uint64, fileNum base.FileNum) interface{} +var SSTableCacheOpts func(cacheID uint64, fileNum base.DiskFileNum) interface{} // SSTableRawTombstonesOpt is a sstable.Reader option for disabling // fragmentation of the range tombstones returned by diff --git a/vendor/github.com/cockroachdb/pebble/internal/rangekey/coalesce.go b/vendor/github.com/cockroachdb/pebble/internal/rangekey/coalesce.go index a25b64d..c0456bb 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/rangekey/coalesce.go +++ b/vendor/github.com/cockroachdb/pebble/internal/rangekey/coalesce.go @@ -52,15 +52,15 @@ import ( // │ // ╰── .Next type UserIteratorConfig struct { - snapshot uint64 - comparer *base.Comparer - miter keyspan.MergingIter - biter keyspan.BoundedIter - diter keyspan.DefragmentingIter - liters [manifest.NumLevels]keyspan.LevelIter - litersUsed int - onlySets bool - bufs *Buffers + snapshot uint64 + comparer *base.Comparer + miter keyspan.MergingIter + biter keyspan.BoundedIter + diter keyspan.DefragmentingIter + liters [manifest.NumLevels]keyspan.LevelIter + litersUsed int + internalKeys bool + bufs *Buffers } // Buffers holds various buffers used for range key iteration. They're exposed @@ -79,9 +79,10 @@ func (bufs *Buffers) PrepareForReuse() { // Init initializes the range key iterator stack for user iteration. The // resulting fragment iterator applies range key semantics, defragments spans -// according to their user-observable state and, if onlySets = true, removes all +// according to their user-observable state and, if !internalKeys, removes all // Keys other than RangeKeySets describing the current state of range keys. The -// resulting spans contain Keys sorted by Suffix. +// resulting spans contain Keys sorted by suffix (unless internalKeys is true, +// in which case they remain sorted by trailer descending). // // The snapshot sequence number parameter determines which keys are visible. Any // keys not visible at the provided snapshot are ignored. @@ -91,16 +92,20 @@ func (ui *UserIteratorConfig) Init( lower, upper []byte, hasPrefix *bool, prefix *[]byte, - onlySets bool, + internalKeys bool, bufs *Buffers, iters ...keyspan.FragmentIterator, ) keyspan.FragmentIterator { ui.snapshot = snapshot ui.comparer = comparer - ui.onlySets = onlySets + ui.internalKeys = internalKeys ui.miter.Init(comparer.Compare, ui, &bufs.merging, iters...) ui.biter.Init(comparer.Compare, comparer.Split, &ui.miter, lower, upper, hasPrefix, prefix) - ui.diter.Init(comparer, &ui.biter, ui, keyspan.StaticDefragmentReducer, &bufs.defragmenting) + if internalKeys { + ui.diter.Init(comparer, &ui.biter, keyspan.DefragmentInternal, keyspan.StaticDefragmentReducer, &bufs.defragmenting) + } else { + ui.diter.Init(comparer, &ui.biter, ui, keyspan.StaticDefragmentReducer, &bufs.defragmenting) + } ui.litersUsed = 0 ui.bufs = bufs return &ui.diter @@ -136,7 +141,8 @@ func (ui *UserIteratorConfig) SetBounds(lower, upper []byte) { // of unset keys, removal of keys overwritten by a set at the same suffix, etc) // and then non-RangeKeySet keys are removed. The resulting transformed spans // only contain RangeKeySets describing the state visible at the provided -// sequence number, and hold their Keys sorted by Suffix. +// sequence number, and hold their Keys sorted by Suffix (except if internalKeys +// is true, then keys remain sorted by trailer. func (ui *UserIteratorConfig) Transform(cmp base.Compare, s keyspan.Span, dst *keyspan.Span) error { // Apply shadowing of keys. dst.Start = s.Start @@ -148,9 +154,16 @@ func (ui *UserIteratorConfig) Transform(cmp base.Compare, s keyspan.Span, dst *k if err := coalesce(ui.comparer.Equal, &ui.bufs.sortBuf, ui.snapshot, s.Keys); err != nil { return err } - // During user iteration over range keys, unsets and deletes don't matter. - // Remove them if onlySets = true. This step helps logical defragmentation - // during iteration. + if ui.internalKeys { + if s.KeysOrder != keyspan.ByTrailerDesc { + panic("unexpected key ordering in UserIteratorTransform with internalKeys = true") + } + dst.Keys = ui.bufs.sortBuf.Keys + keyspan.SortKeysByTrailer(&dst.Keys) + return nil + } + // During user iteration over range keys, unsets and deletes don't matter. This + // step helps logical defragmentation during iteration. keys := ui.bufs.sortBuf.Keys dst.Keys = dst.Keys[:0] for i := range keys { @@ -164,17 +177,11 @@ func (ui *UserIteratorConfig) Transform(cmp base.Compare, s keyspan.Span, dst *k if invariants.Enabled && len(dst.Keys) > 0 && cmp(dst.Keys[len(dst.Keys)-1].Suffix, keys[i].Suffix) > 0 { panic("pebble: keys unexpectedly not in ascending suffix order") } - if ui.onlySets { - // Skip. - continue - } - dst.Keys = append(dst.Keys, keys[i]) + // Skip. + continue case base.InternalKeyKindRangeKeyDelete: - if ui.onlySets { - // Skip. - continue - } - dst.Keys = append(dst.Keys, keys[i]) + // Skip. + continue default: return base.CorruptionErrorf("pebble: unrecognized range key kind %s", keys[i].Kind()) } @@ -193,6 +200,10 @@ func (ui *UserIteratorConfig) Transform(cmp base.Compare, s keyspan.Span, dst *k // sequence numbers). It's intended for use during user iteration, when the // wrapped keyspan iterator is merging spans across all levels of the LSM. func (ui *UserIteratorConfig) ShouldDefragment(equal base.Equal, a, b *keyspan.Span) bool { + // This method is not called with internalKeys = true. + if ui.internalKeys { + panic("unexpected call to ShouldDefragment with internalKeys = true") + } // This implementation must only be used on spans that have transformed by // ui.Transform. The transform applies shadowing, removes all keys besides // the resulting Sets and sorts the keys by suffix. Since shadowing has been @@ -208,8 +219,8 @@ func (ui *UserIteratorConfig) ShouldDefragment(equal base.Equal, a, b *keyspan.S ret := true for i := range a.Keys { if invariants.Enabled { - if ui.onlySets && (a.Keys[i].Kind() != base.InternalKeyKindRangeKeySet || - b.Keys[i].Kind() != base.InternalKeyKindRangeKeySet) { + if a.Keys[i].Kind() != base.InternalKeyKindRangeKeySet || + b.Keys[i].Kind() != base.InternalKeyKindRangeKeySet { panic("pebble: unexpected non-RangeKeySet during defragmentation") } if i > 0 && (ui.comparer.Compare(a.Keys[i].Suffix, a.Keys[i-1].Suffix) < 0 || diff --git a/vendor/github.com/cockroachdb/pebble/internal/rate/rate.go b/vendor/github.com/cockroachdb/pebble/internal/rate/rate.go deleted file mode 100644 index 565626f..0000000 --- a/vendor/github.com/cockroachdb/pebble/internal/rate/rate.go +++ /dev/null @@ -1,387 +0,0 @@ -// Copyright 2015 The Go 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 rate provides a rate limiter. -package rate // import "github.com/cockroachdb/pebble/internal/rate" - -import ( - "context" - "math" - "sync" - "time" - - "github.com/cockroachdb/errors" -) - -// Limit defines the maximum frequency of some events. -// Limit is represented as number of events per second. -// A zero Limit allows no events. -type Limit float64 - -// Inf is the infinite rate limit; it allows all events (even if burst is zero). -const Inf = Limit(math.MaxFloat64) - -// Every converts a minimum time interval between events to a Limit. -func Every(interval time.Duration) Limit { - if interval <= 0 { - return Inf - } - return 1 / Limit(interval.Seconds()) -} - -// A Limiter controls how frequently events are allowed to happen. -// It implements a "token bucket" of size b, initially full and refilled -// at rate r tokens per second. -// Informally, in any large enough time interval, the Limiter limits the -// rate to r tokens per second, with a maximum burst size of b events. -// As a special case, if r == Inf (the infinite rate), b is ignored. -// See https://en.wikipedia.org/wiki/Token_bucket for more about token buckets. -// -// The zero value is a valid Limiter, but it will reject all events. -// Use NewLimiter to create non-zero Limiters. -// -// Limiter has three main methods, Allow, Reserve, and Wait. -// Most callers should use Wait. -// -// Each of the three methods consumes a single token. -// They differ in their behavior when no token is available. -// If no token is available, Allow returns false. -// If no token is available, Reserve returns a reservation for a future token -// and the amount of time the caller must wait before using it. -// If no token is available, Wait blocks until one can be obtained -// or its associated context.Context is canceled. -// -// The methods AllowN, ReserveN, and WaitN consume n tokens. -type Limiter struct { - limit Limit - burst int - - mu sync.Mutex - tokens float64 - // last is the last time the limiter's tokens field was updated - last time.Time - // lastEvent is the latest time of a rate-limited event (past or future) - lastEvent time.Time -} - -// Limit returns the maximum overall event rate. -func (lim *Limiter) Limit() Limit { - lim.mu.Lock() - defer lim.mu.Unlock() - return lim.limit -} - -// Burst returns the maximum burst size. Burst is the maximum number of tokens -// that can be consumed in a single call to Allow, Reserve, or Wait, so higher -// Burst values allow more events to happen at once. -// A zero Burst allows no events, unless limit == Inf. -func (lim *Limiter) Burst() int { - return lim.burst -} - -// NewLimiter returns a new Limiter that allows events up to rate r and permits -// bursts of at most b tokens. -func NewLimiter(r Limit, b int) *Limiter { - return &Limiter{ - limit: r, - burst: b, - } -} - -// Allow is shorthand for AllowN(time.Now(), 1). -func (lim *Limiter) Allow() bool { - return lim.AllowN(time.Now(), 1) -} - -// AllowN reports whether n events may happen at time now. -// Use this method if you intend to drop / skip events that exceed the rate limit. -// Otherwise use Reserve or Wait. -func (lim *Limiter) AllowN(now time.Time, n int) bool { - return lim.reserveN(now, n, 0).ok -} - -// A Reservation holds information about events that are permitted by a Limiter to happen after a delay. -// A Reservation may be canceled, which may enable the Limiter to permit additional events. -type Reservation struct { - ok bool - lim *Limiter - tokens int - timeToAct time.Time - // This is the Limit at reservation time, it can change later. - limit Limit -} - -// OK returns whether the limiter can provide the requested number of tokens -// within the maximum wait time. If OK is false, Delay returns InfDuration, and -// Cancel does nothing. -func (r *Reservation) OK() bool { - return r.ok -} - -// Delay is shorthand for DelayFrom(time.Now()). -func (r *Reservation) Delay() time.Duration { - return r.DelayFrom(time.Now()) -} - -// InfDuration is the duration returned by Delay when a Reservation is not OK. -const InfDuration = time.Duration(1<<63 - 1) - -// DelayFrom returns the duration for which the reservation holder must wait -// before taking the reserved action. Zero duration means act immediately. -// InfDuration means the limiter cannot grant the tokens requested in this -// Reservation within the maximum wait time. -func (r *Reservation) DelayFrom(now time.Time) time.Duration { - if !r.ok { - return InfDuration - } - delay := r.timeToAct.Sub(now) - if delay < 0 { - return 0 - } - return delay -} - -// Cancel is shorthand for CancelAt(time.Now()). -func (r *Reservation) Cancel() { - r.CancelAt(time.Now()) -} - -// CancelAt indicates that the reservation holder will not perform the reserved action -// and reverses the effects of this Reservation on the rate limit as much as possible, -// considering that other reservations may have already been made. -func (r *Reservation) CancelAt(now time.Time) { - if !r.ok { - return - } - - r.lim.mu.Lock() - defer r.lim.mu.Unlock() - - if r.lim.limit == Inf || r.tokens == 0 || r.timeToAct.Before(now) { - return - } - - // calculate tokens to restore - // The duration between lim.lastEvent and r.timeToAct tells us how many tokens were reserved - // after r was obtained. These tokens should not be restored. - restoreTokens := float64(r.tokens) - r.limit.tokensFromDuration(r.lim.lastEvent.Sub(r.timeToAct)) - if restoreTokens <= 0 { - return - } - // advance time to now - now, _, tokens := r.lim.advance(now) - // calculate new number of tokens - tokens += restoreTokens - if burst := float64(r.lim.burst); tokens > burst { - tokens = burst - } - // update state - r.lim.last = now - r.lim.tokens = tokens - if r.timeToAct == r.lim.lastEvent { - prevEvent := r.timeToAct.Add(r.limit.durationFromTokens(float64(-r.tokens))) - if !prevEvent.Before(now) { - r.lim.lastEvent = prevEvent - } - } -} - -// Reserve is shorthand for ReserveN(time.Now(), 1). -func (lim *Limiter) Reserve() Reservation { - return lim.ReserveN(time.Now(), 1) -} - -// ReserveN returns a Reservation that indicates how long the caller must wait before n events happen. -// The Limiter takes this Reservation into account when allowing future events. -// ReserveN returns false if n exceeds the Limiter's burst size. -// Usage example: -// -// r := lim.ReserveN(time.Now(), 1) -// if !r.OK() { -// // Not allowed to act! Did you remember to set lim.burst to be > 0 ? -// return -// } -// time.Sleep(r.Delay()) -// Act() -// -// Use this method if you wish to wait and slow down in accordance with the rate limit without dropping events. -// If you need to respect a deadline or cancel the delay, use Wait instead. -// To drop or skip events exceeding rate limit, use Allow instead. -func (lim *Limiter) ReserveN(now time.Time, n int) Reservation { - return lim.reserveN(now, n, InfDuration) -} - -// Wait is shorthand for WaitN(ctx, 1). -func (lim *Limiter) Wait(ctx context.Context) (err error) { - return lim.WaitN(ctx, 1) -} - -// WaitN blocks until lim permits n events to happen. -// It returns an error if n exceeds the Limiter's burst size, the Context is -// canceled, or the expected wait time exceeds the Context's Deadline. -func (lim *Limiter) WaitN(ctx context.Context, n int) (err error) { - if n > lim.burst && lim.limit != Inf { - return errors.Errorf("rate: Wait(n=%d) exceeds limiter's burst %d", errors.Safe(n), errors.Safe(lim.burst)) - } - // Check if ctx is already cancelled - select { - case <-ctx.Done(): - return ctx.Err() - default: - } - // Determine wait limit - now := time.Now() - waitLimit := InfDuration - if deadline, ok := ctx.Deadline(); ok { - waitLimit = deadline.Sub(now) - } - // Reserve - r := lim.reserveN(now, n, waitLimit) - if !r.ok { - return errors.Errorf("rate: Wait(n=%d) would exceed context deadline", errors.Safe(n)) - } - // Wait - d := r.DelayFrom(now) - if d <= 0 { - return nil - } - t := time.NewTimer(d) - defer t.Stop() - select { - case <-t.C: - // We can proceed. - return nil - case <-ctx.Done(): - // Context was canceled before we could proceed. Cancel the - // reservation, which may permit other events to proceed sooner. - r.Cancel() - return ctx.Err() - } -} - -// Delay is shorthand for DelayN(time.Now(), 1). -func (lim *Limiter) Delay() time.Duration { - return lim.DelayN(time.Now(), 1) -} - -// DelayN returns the delay to wait to permit n events to happen. Zero duration -// means act immediately. InfDuration means the limiter cannot grant the tokens -// requested within the maximum wait time. -func (lim *Limiter) DelayN(now time.Time, n int) time.Duration { - r := lim.reserveN(now, n, InfDuration) - return r.DelayFrom(now) -} - -// SetLimit is shorthand for SetLimitAt(time.Now(), newLimit). -func (lim *Limiter) SetLimit(newLimit Limit) { - lim.SetLimitAt(time.Now(), newLimit) -} - -// SetLimitAt sets a new Limit for the limiter. The new Limit, and Burst, may be violated -// or underutilized by those which reserved (using Reserve or Wait) but did not yet act -// before SetLimitAt was called. -func (lim *Limiter) SetLimitAt(now time.Time, newLimit Limit) { - lim.mu.Lock() - defer lim.mu.Unlock() - - now, _, tokens := lim.advance(now) - - lim.last = now - lim.tokens = tokens - lim.limit = newLimit -} - -// reserveN is a helper method for AllowN, ReserveN, and WaitN. -// maxFutureReserve specifies the maximum reservation wait duration allowed. -// reserveN returns Reservation, not *Reservation, to avoid allocation in AllowN and WaitN. -func (lim *Limiter) reserveN(now time.Time, n int, maxFutureReserve time.Duration) Reservation { - lim.mu.Lock() - - if lim.limit == Inf { - lim.mu.Unlock() - return Reservation{ - ok: true, - lim: lim, - tokens: n, - timeToAct: now, - } - } - - now, last, tokens := lim.advance(now) - - // Calculate the remaining number of tokens resulting from the request. - tokens -= float64(n) - - // Calculate the wait duration - var waitDuration time.Duration - if tokens < 0 { - waitDuration = lim.limit.durationFromTokens(-tokens) - } - - // Decide result - ok := n <= lim.burst && waitDuration <= maxFutureReserve - - // Prepare reservation - r := Reservation{ - ok: ok, - lim: lim, - limit: lim.limit, - } - if ok { - r.tokens = n - r.timeToAct = now.Add(waitDuration) - } - - // Update state - if ok { - lim.last = now - lim.tokens = tokens - lim.lastEvent = r.timeToAct - } else { - lim.last = last - } - - lim.mu.Unlock() - return r -} - -// advance calculates and returns an updated state for lim resulting from the passage of time. -// lim is not changed. -func (lim *Limiter) advance( - now time.Time, -) (newNow time.Time, newLast time.Time, newTokens float64) { - last := lim.last - if now.Before(last) { - last = now - } - - // Avoid making delta overflow below when last is very old. - maxElapsed := lim.limit.durationFromTokens(float64(lim.burst) - lim.tokens) - elapsed := now.Sub(last) - if elapsed > maxElapsed { - elapsed = maxElapsed - } - - // Calculate the new number of tokens, due to time that passed. - delta := lim.limit.tokensFromDuration(elapsed) - tokens := lim.tokens + delta - if burst := float64(lim.burst); tokens > burst { - tokens = burst - } - - return now, last, tokens -} - -// durationFromTokens is a unit conversion function from the number of tokens to the duration -// of time it takes to accumulate them at a rate of limit tokens per second. -func (limit Limit) durationFromTokens(tokens float64) time.Duration { - seconds := tokens / float64(limit) - return time.Nanosecond * time.Duration(1e9*seconds) -} - -// tokensFromDuration is a unit conversion function from a time duration to the number of tokens -// which could be accumulated during that duration at a rate of limit tokens per second. -func (limit Limit) tokensFromDuration(d time.Duration) float64 { - return d.Seconds() * float64(limit) -} diff --git a/vendor/github.com/cockroachdb/pebble/internal/testkeys/testkeys.go b/vendor/github.com/cockroachdb/pebble/internal/testkeys/testkeys.go index 8ac89e2..fa7b10c 100644 --- a/vendor/github.com/cockroachdb/pebble/internal/testkeys/testkeys.go +++ b/vendor/github.com/cockroachdb/pebble/internal/testkeys/testkeys.go @@ -20,17 +20,27 @@ import ( "strings" "github.com/cockroachdb/pebble/internal/base" + "golang.org/x/exp/constraints" + "golang.org/x/exp/rand" ) const alpha = "abcdefghijklmnopqrstuvwxyz" const suffixDelim = '@' +var inverseAlphabet = make(map[byte]int64, len(alpha)) + +func init() { + for i := range alpha { + inverseAlphabet[alpha[i]] = int64(i) + } +} + // MaxSuffixLen is the maximum length of a suffix generated by this package. -var MaxSuffixLen = 1 + len(fmt.Sprintf("%d", math.MaxInt64)) +var MaxSuffixLen = 1 + len(fmt.Sprintf("%d", int64(math.MaxInt64))) // Comparer is the comparer for test keys generated by this package. -var Comparer *base.Comparer = &base.Comparer{ +var Comparer = &base.Comparer{ Compare: compare, Equal: func(a, b []byte) bool { return compare(a, b) == 0 }, AbbreviatedKey: func(k []byte) uint64 { @@ -136,7 +146,7 @@ func compareTimestamps(a, b []byte) int { // Keyspace describes a finite keyspace of unsuffixed test keys. type Keyspace interface { // Count returns the number of keys that exist within this keyspace. - Count() int + Count() int64 // MaxLen returns the maximum length, in bytes, of a key within this // keyspace. This is only guaranteed to return an upper bound. @@ -144,20 +154,21 @@ type Keyspace interface { // Slice returns the sub-keyspace from index i, inclusive, to index j, // exclusive. The receiver is unmodified. - Slice(i, j int) Keyspace + Slice(i, j int64) Keyspace // EveryN returns a key space that includes 1 key for every N keys in the // original keyspace. The receiver is unmodified. - EveryN(n int) Keyspace + EveryN(n int64) Keyspace - key(buf []byte, i int) int + // key writes the i-th key to the buffer and returns the length. + key(buf []byte, i int64) int } // Divvy divides the provided keyspace into N equal portions, containing // disjoint keys evenly distributed across the keyspace. -func Divvy(ks Keyspace, n int) []Keyspace { +func Divvy(ks Keyspace, n int64) []Keyspace { ret := make([]Keyspace, n) - for i := 0; i < n; i++ { + for i := int64(0); i < n; i++ { ret[i] = ks.Slice(i, ks.Count()).EveryN(n) } return ret @@ -175,7 +186,7 @@ func Alpha(maxLength int) Keyspace { // KeyAt returns the i-th key within the keyspace with a suffix encoding the // timestamp t. -func KeyAt(k Keyspace, i int, t int) []byte { +func KeyAt(k Keyspace, i int64, t int64) []byte { b := make([]byte, k.MaxLen()+MaxSuffixLen) return b[:WriteKeyAt(b, k, i, t)] } @@ -183,20 +194,20 @@ func KeyAt(k Keyspace, i int, t int) []byte { // WriteKeyAt writes the i-th key within the keyspace to the buffer dst, with a // suffix encoding the timestamp t suffix. It returns the number of bytes // written. -func WriteKeyAt(dst []byte, k Keyspace, i int, t int) int { +func WriteKeyAt(dst []byte, k Keyspace, i int64, t int64) int { n := WriteKey(dst, k, i) n += WriteSuffix(dst[n:], t) return n } // Suffix returns the test keys suffix representation of timestamp t. -func Suffix(t int) []byte { +func Suffix(t int64) []byte { b := make([]byte, MaxSuffixLen) return b[:WriteSuffix(b, t)] } // SuffixLen returns the exact length of the given suffix when encoded. -func SuffixLen(t int) int { +func SuffixLen(t int64) int { // Begin at 1 for the '@' delimiter, 1 for a single digit. n := 2 t /= 10 @@ -208,42 +219,42 @@ func SuffixLen(t int) int { } // ParseSuffix returns the integer representation of the encoded suffix. -func ParseSuffix(s []byte) (int, error) { - return strconv.Atoi(strings.TrimPrefix(string(s), string(suffixDelim))) +func ParseSuffix(s []byte) (int64, error) { + return strconv.ParseInt(strings.TrimPrefix(string(s), string(suffixDelim)), 10, 64) } // WriteSuffix writes the test keys suffix representation of timestamp t to dst, // returning the number of bytes written. -func WriteSuffix(dst []byte, t int) int { +func WriteSuffix(dst []byte, t int64) int { dst[0] = suffixDelim n := 1 - n += len(strconv.AppendInt(dst[n:n], int64(t), 10)) + n += len(strconv.AppendInt(dst[n:n], t, 10)) return n } // Key returns the i-th unsuffixed key within the keyspace. -func Key(k Keyspace, i int) []byte { +func Key(k Keyspace, i int64) []byte { b := make([]byte, k.MaxLen()) return b[:k.key(b, i)] } // WriteKey writes the i-th unsuffixed key within the keyspace to the buffer dst. It // returns the number of bytes written. -func WriteKey(dst []byte, k Keyspace, i int) int { +func WriteKey(dst []byte, k Keyspace, i int64) int { return k.key(dst, i) } type alphabet struct { alphabet []byte maxLength int - headSkip int - tailSkip int - increment int + headSkip int64 + tailSkip int64 + increment int64 } -func (a alphabet) Count() int { +func (a alphabet) Count() int64 { // Calculate the total number of keys, ignoring the increment. - total := (keyCount(len(a.alphabet), a.maxLength) - a.headSkip - a.tailSkip) + total := keyCount(len(a.alphabet), a.maxLength) - a.headSkip - a.tailSkip // The increment dictates that we take every N keys, where N = a.increment. // Consider a total containing the 5 keys: @@ -263,20 +274,25 @@ func (a alphabet) MaxLen() int { return a.maxLength } -func (a alphabet) Slice(i, j int) Keyspace { +func (a alphabet) Slice(i, j int64) Keyspace { s := a s.headSkip += i s.tailSkip += a.Count() - j return s } -func (a alphabet) EveryN(n int) Keyspace { +func (a alphabet) EveryN(n int64) Keyspace { s := a s.increment *= n return s } -func keyCount(n, l int) int { +func keyCount(n, l int) int64 { + if n == 0 { + return 0 + } else if n == 1 { + return int64(l) + } // The number of representable keys in the keyspace is a function of the // length of the alphabet n and the max key length l. Consider how the // number of representable keys grows as l increases: @@ -286,10 +302,10 @@ func keyCount(n, l int) int { // l = 3: n + n^2 + n^3 // ... // Σ i=(1...l) n^i = n*(n^l - 1)/(n-1) - return (n * (int(math.Pow(float64(n), float64(l))) - 1)) / (n - 1) + return (int64(n) * (int64(math.Pow(float64(n), float64(l))) - 1)) / int64(n-1) } -func (a alphabet) key(buf []byte, idx int) int { +func (a alphabet) key(buf []byte, idx int64) int { // This function generates keys of length 1..maxKeyLength, pulling // characters from the alphabet. The idx determines which key to generate, // generating the i-th lexicographically next key. @@ -308,14 +324,14 @@ func (a alphabet) key(buf []byte, idx int) int { keyCount(len(a.alphabet), a.maxLength)) } -func generateAlphabetKey(buf, alphabet []byte, i, keyCount int) int { +func generateAlphabetKey(buf, alphabet []byte, i, keyCount int64) int { if keyCount == 0 || i > keyCount || i < 0 { return 0 } // Of the keyCount keys in the generative keyspace, how many are there // starting with a particular character? - keysPerCharacter := keyCount / len(alphabet) + keysPerCharacter := keyCount / int64(len(alphabet)) // Find the character that the key at index i starts with and set it. characterIdx := i / keysPerCharacter @@ -347,3 +363,156 @@ func generateAlphabetKey(buf, alphabet []byte, i, keyCount int) int { i = i - keysPerCharacter*characterIdx - 1 return 1 + generateAlphabetKey(buf[1:], alphabet, i, keysPerCharacter-1) } + +// computeAlphabetKeyIndex computes the inverse of generateAlphabetKey, +// returning the index of a particular key, given the provided alphabet and max +// length of a key. +// +// len(key) must be ≥ 1. +func computeAlphabetKeyIndex(key []byte, alphabet map[byte]int64, n int) int64 { + i, ok := alphabet[key[0]] + if !ok { + panic(fmt.Sprintf("unrecognized alphabet character %v", key[0])) + } + // How many keys exist that start with the preceding i characters? Each of + // the i characters themselves are a key, plus the count of all the keys + // with one less character for each. + ret := i + i*keyCount(len(alphabet), n-1) + if len(key) > 1 { + ret += 1 + computeAlphabetKeyIndex(key[1:], alphabet, n-1) + } + return ret +} + +func abs(a int64) int64 { + if a < 0 { + return -a + } + return a +} + +// RandomSeparator returns a random alphabetic key k such that a < k < b, +// pulling randomness from the provided random number generator. If dst is +// provided and the generated key fits within dst's capacity, the returned slice +// will use dst's memory. +// +// If a prefix P exists such that Prefix(a) < P < Prefix(b), the generated key +// will consist of the prefix P appended with the provided suffix. A zero suffix +// generates an unsuffixed key. If no such prefix P exists, RandomSeparator will +// try to find a key k with either Prefix(a) or Prefix(b) such that a < k < b, +// but the generated key will not use the provided suffix. Note that it's +// possible that no separator key exists (eg, a='a@2', b='a@1'), in which case +// RandomSeparator returns nil. +// +// If RandomSeparator generates a new prefix, the generated prefix will have +// length at most MAX(maxLength, len(Prefix(a)), len(Prefix(b))). +// +// RandomSeparator panics if a or b fails to decode. +func RandomSeparator(dst, a, b []byte, suffix int64, maxLength int, rng *rand.Rand) []byte { + if Comparer.Compare(a, b) >= 0 { + return nil + } + + // Determine both keys' logical prefixes and suffixes. + ai := Comparer.Split(a) + bi := Comparer.Split(b) + ap := a[:ai] + bp := b[:bi] + maxLength = max[int](maxLength, max[int](len(ap), len(bp))) + var as, bs int64 + var err error + if ai != len(a) { + as, err = ParseSuffix(a[ai:]) + if err != nil { + panic(fmt.Sprintf("failed to parse suffix of %q", a)) + } + } + if bi != len(b) { + bs, err = ParseSuffix(b[bi:]) + if err != nil { + panic(fmt.Sprintf("failed to parse suffix of %q", b)) + } + } + + apIdx := computeAlphabetKeyIndex(ap, inverseAlphabet, maxLength) + bpIdx := computeAlphabetKeyIndex(bp, inverseAlphabet, maxLength) + diff := bpIdx - apIdx + generatedIdx := bpIdx + if diff > 0 { + var add int64 = diff + 1 + var start int64 = apIdx + if as == 1 { + // There's no expressible key with prefix a greater than a@1. So, + // exclude ap. + start = apIdx + 1 + add = diff + } + if bs == 0 { + // No key with prefix b can sort before b@0. We don't want to pick b. + add-- + } + // We're allowing generated id to be in the range [start, start + add - 1]. + if start > start+add-1 { + return nil + } + // If we can generate a key which is actually in the middle of apIdx + // and bpIdx use it so that we don't have to bother about timestamps. + generatedIdx = rng.Int63n(add) + start + for diff > 1 && generatedIdx == apIdx || generatedIdx == bpIdx { + generatedIdx = rng.Int63n(add) + start + } + } + + switch { + case generatedIdx == apIdx && generatedIdx == bpIdx: + if abs(bs-as) <= 1 { + // There's no expressible suffix between the two, and there's no + // possible separator key. + return nil + } + // The key b is >= key a, but has the same prefix, so b must have the + // smaller timestamp, unless a has timestamp of 0. + // + // NB: The zero suffix (suffix-less) sorts before all other suffixes, so + // any suffix we generate will be greater than it. + if as == 0 { + // bs > as + suffix = bs + rng.Int63n(10) + 1 + } else { + // bs < as. + // Generate suffix in range [bs + 1, as - 1] + suffix = bs + 1 + rng.Int63n(as-bs-1) + } + case generatedIdx == apIdx: + // NB: The zero suffix (suffix-less) sorts before all other suffixes, so + // any suffix we generate will be greater than it. + if as == 0 && suffix == 0 { + suffix++ + } else if as != 0 && suffix >= as { + suffix = rng.Int63n(as) + } + case generatedIdx == bpIdx: + if suffix <= bs { + suffix = bs + rng.Int63n(10) + 1 + } + } + if sz := maxLength + SuffixLen(suffix); cap(dst) < sz { + dst = make([]byte, sz) + } else { + dst = dst[:cap(dst)] + } + var w int + if suffix == 0 { + w = WriteKey(dst, Alpha(maxLength), generatedIdx) + } else { + w = WriteKeyAt(dst, Alpha(maxLength), generatedIdx, suffix) + } + return dst[:w] +} + +func max[I constraints.Ordered](a, b I) I { + if b > a { + return b + } + return a +} diff --git a/vendor/github.com/cockroachdb/pebble/internal_iterator.go b/vendor/github.com/cockroachdb/pebble/internal_iterator.go deleted file mode 100644 index f399a18..0000000 --- a/vendor/github.com/cockroachdb/pebble/internal_iterator.go +++ /dev/null @@ -1,332 +0,0 @@ -// Copyright 2023 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 ( - "context" - - "github.com/cockroachdb/pebble/internal/base" - "github.com/cockroachdb/pebble/internal/keyspan" - "github.com/cockroachdb/pebble/internal/manifest" -) - -// scanInternalIterator is an iterator that returns all internal keys instead of -// collapsing them by user keys. For instance, an InternalKeyKindDelete would be -// returned as an InternalKeyKindDelete instead of the iterator skipping over to -// the next key. Useful if an external user of Pebble needs to observe and -// rebuild Pebble's history of internal keys, such as in node-to-node -// replication. For use with {db,snapshot}.ScanInternal(). -// -// scanInternalIterator is allowed to ignorepoint keys deleted by range deletions, -// and range keys shadowed by a range key unset or delete, however it *must* -// return the range delete as well as the range key unset/delete that did the -// shadowing. -type scanInternalIterator struct { - opts IterOptions - comparer *base.Comparer - iter internalIterator - readState *readState - rangeKey *iteratorRangeKeyState - pointKeyIter keyspan.InterleavingIter - iterKey *InternalKey - iterValue LazyValue - alloc *iterAlloc - newIters tableNewIters - newIterRangeKey keyspan.TableNewSpanIter - seqNum uint64 - - // boundsBuf holds two buffers used to store the lower and upper bounds. - // Whenever the InternalIterator's bounds change, the new bounds are copied - // into boundsBuf[boundsBufIdx]. The two bounds share a slice to reduce - // allocations. opts.LowerBound and opts.UpperBound point into this slice. - boundsBuf [2][]byte - boundsBufIdx int -} - -// TODO(sumeer): scanInternalImpl is the implementation for the user-facing -// ScanInternal. Make it accept a context parameter, and plumb it through for -// tracing support. - -func scanInternalImpl( - lower []byte, - iter *scanInternalIterator, - visitPointKey func(key *InternalKey, value LazyValue) error, - visitRangeDel func(start, end []byte, seqNum uint64) error, - visitRangeKey func(start, end []byte, keys []keyspan.Key) error, -) error { - for valid := iter.seekGE(lower); valid && iter.error() == nil; valid = iter.next() { - key := iter.unsafeKey() - - switch key.Kind() { - case InternalKeyKindRangeKeyDelete, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeySet: - span := iter.unsafeSpan() - if err := visitRangeKey(span.Start, span.End, span.Keys); err != nil { - return err - } - case InternalKeyKindRangeDelete: - rangeDel := iter.unsafeRangeDel() - if err := visitRangeDel(rangeDel.Start, rangeDel.End, rangeDel.LargestSeqNum()); err != nil { - return err - } - default: - val := iter.lazyValue() - if s := iter.pointKeyIter.Span(); s != nil && s.CoversAt(iter.seqNum, key.SeqNum()) { - // Key deleted by a range deletion. Skip it. - continue - } - if err := visitPointKey(key, val); err != nil { - return err - } - } - } - - return nil -} - -// constructPointIter constructs a merging iterator and sets i.iter to it. -func (i *scanInternalIterator) constructPointIter(memtables flushableList, buf *iterAlloc) { - // Merging levels and levels from iterAlloc. - mlevels := buf.mlevels[:0] - levels := buf.levels[:0] - - // We compute the number of levels needed ahead of time and reallocate a slice if - // the array from the iterAlloc isn't large enough. Doing this allocation once - // should improve the performance. - numMergingLevels := len(memtables) - numLevelIters := 0 - - current := i.readState.current - numMergingLevels += len(current.L0SublevelFiles) - numLevelIters += len(current.L0SublevelFiles) - for level := 1; level < len(current.Levels); level++ { - if current.Levels[level].Empty() { - continue - } - numMergingLevels++ - numLevelIters++ - } - - if numMergingLevels > cap(mlevels) { - mlevels = make([]mergingIterLevel, 0, numMergingLevels) - } - if numLevelIters > cap(levels) { - levels = make([]levelIter, 0, numLevelIters) - } - // TODO(bilal): Push these into the iterAlloc buf. - var rangeDelMiter keyspan.MergingIter - rangeDelIters := make([]keyspan.FragmentIterator, 0, numMergingLevels) - rangeDelLevels := make([]keyspan.LevelIter, 0, numLevelIters) - - // Next are the memtables. - for j := len(memtables) - 1; j >= 0; j-- { - mem := memtables[j] - mlevels = append(mlevels, mergingIterLevel{ - iter: mem.newIter(&i.opts), - }) - if rdi := mem.newRangeDelIter(&i.opts); rdi != nil { - rangeDelIters = append(rangeDelIters, rdi) - } - } - - // Next are the file levels: L0 sub-levels followed by lower levels. - mlevelsIndex := len(mlevels) - levelsIndex := len(levels) - mlevels = mlevels[:numMergingLevels] - levels = levels[:numLevelIters] - rangeDelLevels = rangeDelLevels[:numLevelIters] - addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) { - li := &levels[levelsIndex] - rli := &rangeDelLevels[levelsIndex] - - li.init( - context.Background(), i.opts, i.comparer.Compare, i.comparer.Split, i.newIters, files, level, - internalIterOpts{}) - li.initBoundaryContext(&mlevels[mlevelsIndex].levelIterBoundaryContext) - mlevels[mlevelsIndex].iter = li - rli.Init(keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters}, - i.comparer.Compare, tableNewRangeDelIter(context.Background(), i.newIters), files, level, - manifest.KeyTypePoint) - rangeDelIters = append(rangeDelIters, rli) - - levelsIndex++ - mlevelsIndex++ - } - - // Add level iterators for the L0 sublevels, iterating from newest to - // oldest. - for i := len(current.L0SublevelFiles) - 1; i >= 0; i-- { - addLevelIterForFiles(current.L0SublevelFiles[i].Iter(), manifest.L0Sublevel(i)) - } - - // Add level iterators for the non-empty non-L0 levels. - for level := 1; level < numLevels; level++ { - if current.Levels[level].Empty() { - continue - } - addLevelIterForFiles(current.Levels[level].Iter(), manifest.Level(level)) - } - buf.merging.init(&i.opts, &InternalIteratorStats{}, i.comparer.Compare, i.comparer.Split, mlevels...) - buf.merging.snapshot = i.seqNum - rangeDelMiter.Init(i.comparer.Compare, keyspan.VisibleTransform(i.seqNum), new(keyspan.MergingBuffers), rangeDelIters...) - i.pointKeyIter.Init(i.comparer, &buf.merging, &rangeDelMiter, nil /* mask */, i.opts.LowerBound, i.opts.UpperBound) - i.iter = &i.pointKeyIter -} - -// constructRangeKeyIter constructs the range-key iterator stack, populating -// i.rangeKey.rangeKeyIter with the resulting iterator. This is similar to -// Iterator.constructRangeKeyIter, except it doesn't handle batches and ensures -// iterConfig does *not* elide unsets/deletes. -func (i *scanInternalIterator) constructRangeKeyIter() { - // We want the bounded iter from iterConfig, but not the collapsing of - // RangeKeyUnsets and RangeKeyDels. - i.rangeKey.rangeKeyIter = i.rangeKey.iterConfig.Init( - i.comparer, i.seqNum, i.opts.LowerBound, i.opts.UpperBound, - nil /* hasPrefix */, nil /* prefix */, false, /* onlySets */ - &i.rangeKey.rangeKeyBuffers.internal) - - // Next are the flushables: memtables and large batches. - for j := len(i.readState.memtables) - 1; j >= 0; j-- { - mem := i.readState.memtables[j] - // We only need to read from memtables which contain sequence numbers older - // than seqNum. - if logSeqNum := mem.logSeqNum; logSeqNum >= i.seqNum { - continue - } - if rki := mem.newRangeKeyIter(&i.opts); rki != nil { - i.rangeKey.iterConfig.AddLevel(rki) - } - } - - current := i.readState.current - // Next are the file levels: L0 sub-levels followed by lower levels. - // - // Add file-specific iterators for L0 files containing range keys. This is less - // efficient than using levelIters for sublevels of L0 files containing - // range keys, but range keys are expected to be sparse anyway, reducing the - // cost benefit of maintaining a separate L0Sublevels instance for range key - // files and then using it here. - // - // NB: We iterate L0's files in reverse order. They're sorted by - // LargestSeqNum ascending, and we need to add them to the merging iterator - // in LargestSeqNum descending to preserve the merging iterator's invariants - // around Key Trailer order. - iter := current.RangeKeyLevels[0].Iter() - for f := iter.Last(); f != nil; f = iter.Prev() { - spanIterOpts := &keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} - spanIter, err := i.newIterRangeKey(f, spanIterOpts) - if err != nil { - i.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err}) - continue - } - i.rangeKey.iterConfig.AddLevel(spanIter) - } - - // Add level iterators for the non-empty non-L0 levels. - for level := 1; level < len(current.RangeKeyLevels); level++ { - if current.RangeKeyLevels[level].Empty() { - continue - } - li := i.rangeKey.iterConfig.NewLevelIter() - spanIterOpts := keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} - li.Init(spanIterOpts, i.comparer.Compare, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(), - manifest.Level(level), manifest.KeyTypeRange) - i.rangeKey.iterConfig.AddLevel(li) - } -} - -// seekGE seeks this iterator to the first key that's greater than or equal -// to the specified user key. -func (i *scanInternalIterator) seekGE(key []byte) bool { - i.iterKey, i.iterValue = i.iter.SeekGE(key, base.SeekGEFlagsNone) - return i.iterKey != nil -} - -// unsafeKey returns the unsafe InternalKey at the current position. The value -// is nil if the iterator is invalid or exhausted. -func (i *scanInternalIterator) unsafeKey() *InternalKey { - return i.iterKey -} - -// lazyValue returns a value pointer to the value at the current iterator -// position. Behaviour undefined if unsafeKey() returns a Range key or Rangedel -// kind key. -func (i *scanInternalIterator) lazyValue() LazyValue { - return i.iterValue -} - -// unsafeRangeDel returns a range key span. Behaviour undefined if UnsafeKey returns -// a non-rangedel kind. -func (i *scanInternalIterator) unsafeRangeDel() *keyspan.Span { - return i.pointKeyIter.Span() -} - -// unsafeSpan returns a range key span. Behaviour undefined if UnsafeKey returns -// a non-rangekey type. -func (i *scanInternalIterator) unsafeSpan() *keyspan.Span { - return i.rangeKey.iiter.Span() -} - -// next advances the iterator in the forward direction, and returns the -// iterator's new validity state. -func (i *scanInternalIterator) next() bool { - i.iterKey, i.iterValue = i.iter.Next() - return i.iterKey != nil -} - -// error returns an error from the internal iterator, if there's any. -func (i *scanInternalIterator) error() error { - return i.iter.Error() -} - -// close closes this iterator, and releases any pooled objects. -func (i *scanInternalIterator) close() error { - if err := i.iter.Close(); err != nil { - return err - } - i.readState.unref() - if i.rangeKey != nil { - i.rangeKey.PrepareForReuse() - *i.rangeKey = iteratorRangeKeyState{ - rangeKeyBuffers: i.rangeKey.rangeKeyBuffers, - } - iterRangeKeyStateAllocPool.Put(i.rangeKey) - i.rangeKey = nil - } - if alloc := i.alloc; alloc != nil { - for j := range i.boundsBuf { - if cap(i.boundsBuf[j]) >= maxKeyBufCacheSize { - alloc.boundsBuf[j] = nil - } else { - alloc.boundsBuf[j] = i.boundsBuf[j] - } - } - *alloc = iterAlloc{ - keyBuf: alloc.keyBuf[:0], - boundsBuf: alloc.boundsBuf, - prefixOrFullSeekKey: alloc.prefixOrFullSeekKey[:0], - } - iterAllocPool.Put(alloc) - i.alloc = nil - } - return nil -} - -func (i *scanInternalIterator) initializeBoundBufs(lower, upper []byte) { - buf := i.boundsBuf[i.boundsBufIdx][:0] - if lower != nil { - buf = append(buf, lower...) - i.opts.LowerBound = buf - } else { - i.opts.LowerBound = nil - } - if upper != nil { - buf = append(buf, upper...) - i.opts.UpperBound = buf[len(buf)-len(upper):] - } else { - i.opts.UpperBound = nil - } - i.boundsBuf[i.boundsBufIdx] = buf - i.boundsBufIdx = 1 - i.boundsBufIdx -} diff --git a/vendor/github.com/cockroachdb/pebble/iterator.go b/vendor/github.com/cockroachdb/pebble/iterator.go index f58588f..2eee5b2 100644 --- a/vendor/github.com/cockroachdb/pebble/iterator.go +++ b/vendor/github.com/cockroachdb/pebble/iterator.go @@ -9,7 +9,6 @@ import ( "context" "io" "sync" - "sync/atomic" "unsafe" "github.com/cockroachdb/errors" @@ -188,7 +187,9 @@ type Iterator struct { comparer base.Comparer iter internalIterator pointIter internalIterator + // Either readState or version is set, but not both. readState *readState + version *version // rangeKey holds iteration state specific to iteration over range keys. // The range key field may be nil if the Iterator has never been configured // to iterate over range keys. Its non-nilness cannot be used to determine @@ -446,6 +447,11 @@ const ( seekPrefixGELastPositioningOp seekGELastPositioningOp seekLTLastPositioningOp + // internalNextOp is a special internal iterator positioning operation used + // by CanDeterministicallySingleDelete. It exists for enforcing requirements + // around calling CanDeterministicallySingleDelete at most once per external + // iterator position. + internalNextOp // invalidatedLastPositionOp is similar to unknownLastPositionOp and the // only reason to distinguish this is for the wider set of SeekGE // optimizations we permit for the external iterator Iterator.forwardOnly @@ -549,6 +555,21 @@ func (i *Iterator) findNextEntry(limit []byte) { return } + // If the user has configured a SkipPoint function, invoke it to see + // whether we should skip over the current user key. + if i.opts.SkipPoint != nil && key.Kind() != InternalKeyKindRangeKeySet && i.opts.SkipPoint(i.iterKey.UserKey) { + // NB: We could call nextUserKey, but in some cases the SkipPoint + // predicate function might be cheaper than nextUserKey's key copy + // and key comparison. This should be the case for MVCC suffix + // comparisons, for example. In the future, we could expand the + // SkipPoint interface to give the implementor more control over + // whether we skip over just the internal key, the user key, or even + // the key prefix. + i.stats.ForwardStepCount[InternalIterCall]++ + i.iterKey, i.iterValue = i.iter.Next() + continue + } + switch key.Kind() { case InternalKeyKindRangeKeySet: // Save the current key. @@ -569,7 +590,10 @@ func (i *Iterator) findNextEntry(limit []byte) { i.iterValidityState = IterValid return - case InternalKeyKindDelete, InternalKeyKindSingleDelete: + case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: + // NB: treating InternalKeyKindSingleDelete as equivalent to DEL is not + // only simpler, but is also necessary for correctness due to + // InternalKeyKindSSTableInternalObsoleteBit. i.nextUserKey() continue @@ -615,6 +639,13 @@ func (i *Iterator) findNextEntry(limit []byte) { } func (i *Iterator) nextPointCurrentUserKey() bool { + // If the user has configured a SkipPoint function and the current user key + // would be skipped by it, there's no need to step forward looking for a + // point key. If we were to find one, it should be skipped anyways. + if i.opts.SkipPoint != nil && i.opts.SkipPoint(i.key) { + return false + } + i.pos = iterPosCurForward i.iterKey, i.iterValue = i.iter.Next() @@ -632,7 +663,10 @@ func (i *Iterator) nextPointCurrentUserKey() bool { i.err = base.CorruptionErrorf("pebble: unexpected range key set mid-user key") return false - case InternalKeyKindDelete, InternalKeyKindSingleDelete: + case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: + // NB: treating InternalKeyKindSingleDelete as equivalent to DEL is not + // only simpler, but is also necessary for correctness due to + // InternalKeyKindSSTableInternalObsoleteBit. return false case InternalKeyKindSet, InternalKeyKindSetWithDelete: @@ -773,52 +807,54 @@ func (i *Iterator) maybeSampleRead() { func (i *Iterator) sampleRead() { var topFile *manifest.FileMetadata topLevel, numOverlappingLevels := numLevels, 0 - if mi, ok := i.iter.(*mergingIter); ok { - if len(mi.levels) > 1 { - mi.ForEachLevelIter(func(li *levelIter) bool { - l := manifest.LevelToInt(li.level) - if f := li.iterFile; f != nil { - var containsKey bool - if i.pos == iterPosNext || i.pos == iterPosCurForward || - i.pos == iterPosCurForwardPaused { - containsKey = i.cmp(f.SmallestPointKey.UserKey, i.key) <= 0 - } else if i.pos == iterPosPrev || i.pos == iterPosCurReverse || - i.pos == iterPosCurReversePaused { - containsKey = i.cmp(f.LargestPointKey.UserKey, i.key) >= 0 - } - // Do nothing if the current key is not contained in f's - // bounds. We could seek the LevelIterator at this level - // to find the right file, but the performance impacts of - // doing that are significant enough to negate the benefits - // of read sampling in the first place. See the discussion - // at: - // https://github.com/cockroachdb/pebble/pull/1041#issuecomment-763226492 - if containsKey { - numOverlappingLevels++ - if numOverlappingLevels >= 2 { - // Terminate the loop early if at least 2 overlapping levels are found. - return true - } - topLevel = l - topFile = f - } + mi := i.merging + if mi == nil { + return + } + if len(mi.levels) > 1 { + mi.ForEachLevelIter(func(li *levelIter) bool { + l := manifest.LevelToInt(li.level) + if f := li.iterFile; f != nil { + var containsKey bool + if i.pos == iterPosNext || i.pos == iterPosCurForward || + i.pos == iterPosCurForwardPaused { + containsKey = i.cmp(f.SmallestPointKey.UserKey, i.key) <= 0 + } else if i.pos == iterPosPrev || i.pos == iterPosCurReverse || + i.pos == iterPosCurReversePaused { + containsKey = i.cmp(f.LargestPointKey.UserKey, i.key) >= 0 } - return false - }) - } + // Do nothing if the current key is not contained in f's + // bounds. We could seek the LevelIterator at this level + // to find the right file, but the performance impacts of + // doing that are significant enough to negate the benefits + // of read sampling in the first place. See the discussion + // at: + // https://github.com/cockroachdb/pebble/pull/1041#issuecomment-763226492 + if containsKey { + numOverlappingLevels++ + if numOverlappingLevels >= 2 { + // Terminate the loop early if at least 2 overlapping levels are found. + return true + } + topLevel = l + topFile = f + } + } + return false + }) } if topFile == nil || topLevel >= numLevels { return } if numOverlappingLevels >= 2 { - allowedSeeks := atomic.AddInt64(&topFile.Atomic.AllowedSeeks, -1) + allowedSeeks := topFile.AllowedSeeks.Add(-1) if allowedSeeks == 0 { // Since the compaction queue can handle duplicates, we can keep // adding to the queue even once allowedSeeks hits 0. // In fact, we NEED to keep adding to the queue, because the queue // is small and evicts older and possibly useful compactions. - atomic.AddInt64(&topFile.Atomic.AllowedSeeks, topFile.InitAllowedSeeks) + topFile.AllowedSeeks.Add(topFile.InitAllowedSeeks) read := readCompaction{ start: topFile.SmallestPointKey.UserKey, @@ -902,6 +938,26 @@ func (i *Iterator) findPrevEntry(limit []byte) { } } + // If the user has configured a SkipPoint function, invoke it to see + // whether we should skip over the current user key. + if i.opts.SkipPoint != nil && key.Kind() != InternalKeyKindRangeKeySet && i.opts.SkipPoint(key.UserKey) { + // NB: We could call prevUserKey, but in some cases the SkipPoint + // predicate function might be cheaper than prevUserKey's key copy + // and key comparison. This should be the case for MVCC suffix + // comparisons, for example. In the future, we could expand the + // SkipPoint interface to give the implementor more control over + // whether we skip over just the internal key, the user key, or even + // the key prefix. + i.stats.ReverseStepCount[InternalIterCall]++ + i.iterKey, i.iterValue = i.iter.Prev() + if limit != nil && i.iterKey != nil && i.cmp(limit, i.iterKey.UserKey) > 0 && !i.rangeKeyWithinLimit(limit) { + i.iterValidityState = IterAtLimit + i.pos = iterPosCurReversePaused + return + } + continue + } + switch key.Kind() { case InternalKeyKindRangeKeySet: // Range key start boundary markers are interleaved with the maximum @@ -930,7 +986,7 @@ func (i *Iterator) findPrevEntry(limit []byte) { // return the key even if the MERGE point key is deleted. rangeKeyBoundary = true - case InternalKeyKindDelete, InternalKeyKindSingleDelete: + case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: i.value = LazyValue{} i.iterValidityState = IterExhausted valueMerger = nil @@ -939,12 +995,12 @@ func (i *Iterator) findPrevEntry(limit []byte) { // Compare with the limit. We could optimize by only checking when // we step to the previous user key, but detecting that requires a // comparison too. Note that this position may already passed a - // number of versions of this user key, but they are all deleted, - // so the fact that a subsequent Prev*() call will not see them is + // number of versions of this user key, but they are all deleted, so + // the fact that a subsequent Prev*() call will not see them is // harmless. Also note that this is the only place in the loop, - // other than the firstLoopIter case above, where we could step - // to a different user key and start processing it for returning - // to the caller. + // other than the firstLoopIter and SkipPoint cases above, where we + // could step to a different user key and start processing it for + // returning to the caller. if limit != nil && i.iterKey != nil && i.cmp(limit, i.iterKey.UserKey) > 0 && !i.rangeKeyWithinLimit(limit) { i.iterValidityState = IterAtLimit i.pos = iterPosCurReversePaused @@ -1093,9 +1149,13 @@ func (i *Iterator) mergeNext(key InternalKey, valueMerger ValueMerger) { return } switch key.Kind() { - case InternalKeyKindDelete, InternalKeyKindSingleDelete: + case InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: // We've hit a deletion tombstone. Return everything up to this // point. + // + // NB: treating InternalKeyKindSingleDelete as equivalent to DEL is not + // only simpler, but is also necessary for correctness due to + // InternalKeyKindSSTableInternalObsoleteBit. return case InternalKeyKindSet, InternalKeyKindSetWithDelete: @@ -1751,7 +1811,8 @@ func (i *Iterator) internalNextPrefix(currKeyPrefixLen int) { i.iterKey, i.iterValue = i.iter.NextPrefix(i.prefixOrFullSeekKey) if invariants.Enabled && i.iterKey != nil { if iterKeyPrefixLen := i.split(i.iterKey.UserKey); i.cmp(i.iterKey.UserKey[:iterKeyPrefixLen], i.prefixOrFullSeekKey) < 0 { - panic("pebble: iter.NextPrefix did not advance beyond the current prefix") + panic(errors.AssertionFailedf("pebble: iter.NextPrefix did not advance beyond the current prefix: now at %q; expected to be geq %q", + i.iterKey, i.prefixOrFullSeekKey)) } } } @@ -2120,6 +2181,9 @@ func (i *Iterator) Value() []byte { // ValueAndErr returns the value, and any error encountered in extracting the value. // REQUIRES: i.Error()==nil and HasPointAndRange() returns true for hasPoint. +// +// The caller should not modify the contents of the returned slice, and its +// contents may change on the next call to Next. func (i *Iterator) ValueAndErr() ([]byte, error) { val, callerOwned, err := i.value.Value(i.lazyValueBuf) if err != nil { @@ -2150,7 +2214,13 @@ func (i *Iterator) RangeKeys() []RangeKeyData { // Valid returns true if the iterator is positioned at a valid key/value pair // and false otherwise. func (i *Iterator) Valid() bool { - return i.iterValidityState == IterValid && !i.requiresReposition + valid := i.iterValidityState == IterValid && !i.requiresReposition + if invariants.Enabled { + if err := i.Error(); valid && err != nil { + panic(errors.WithSecondaryError(errors.AssertionFailedf("pebble: iterator is valid with non-nil Error"), err)) + } + } + return valid } // Error returns any accumulated error. @@ -2215,6 +2285,10 @@ func (i *Iterator) Close() error { i.readState = nil } + if i.version != nil { + i.version.Unref() + } + for _, readers := range i.externalReaders { for _, r := range readers { err = firstError(err, r.Close()) @@ -2407,7 +2481,8 @@ func (i *Iterator) SetOptions(o *IterOptions) { // If either options specify block property filters for an iterator stack, // reconstruct it. if i.pointIter != nil && (closeBoth || len(o.PointKeyFilters) > 0 || len(i.opts.PointKeyFilters) > 0 || - o.RangeKeyMasking.Filter != nil || i.opts.RangeKeyMasking.Filter != nil) { + o.RangeKeyMasking.Filter != nil || i.opts.RangeKeyMasking.Filter != nil || o.SkipPoint != nil || + i.opts.SkipPoint != nil) { i.err = firstError(i.err, i.pointIter.Close()) i.pointIter = nil } @@ -2646,11 +2721,22 @@ func (i *Iterator) CloneWithContext(ctx context.Context, opts CloneOptions) (*It } readState := i.readState - if readState == nil { + vers := i.version + if readState == nil && vers == nil { return nil, errors.Errorf("cannot Clone a closed Iterator") } // i is already holding a ref, so there is no race with unref here. - readState.ref() + // + // TODO(bilal): If the underlying iterator was created on a snapshot, we could + // grab a reference to the current readState instead of reffing the original + // readState. This allows us to release references to some zombie sstables + // and memtables. + if readState != nil { + readState.ref() + } + if vers != nil { + vers.Ref() + } // Bundle various structures under a single umbrella in order to allocate // them together. buf := iterAllocPool.Get().(*iterAlloc) @@ -2662,6 +2748,7 @@ func (i *Iterator) CloneWithContext(ctx context.Context, opts CloneOptions) (*It merge: i.merge, comparer: i.comparer, readState: readState, + version: vers, keyBuf: buf.keyBuf, prefixOrFullSeekKey: buf.prefixOrFullSeekKey, boundsBuf: buf.boundsBuf, @@ -2716,19 +2803,19 @@ func (stats *IteratorStats) SafeFormat(s redact.SafePrinter, verb rune) { s.SafeString(",\n(internal-stats: ") s.Printf("(block-bytes: (total %s, cached %s, read-time %s)), "+ "(points: (count %s, key-bytes %s, value-bytes %s, tombstoned %s))", - humanize.IEC.Uint64(stats.InternalStats.BlockBytes), - humanize.IEC.Uint64(stats.InternalStats.BlockBytesInCache), + humanize.Bytes.Uint64(stats.InternalStats.BlockBytes), + humanize.Bytes.Uint64(stats.InternalStats.BlockBytesInCache), humanize.FormattedString(stats.InternalStats.BlockReadDuration.String()), - humanize.SI.Uint64(stats.InternalStats.PointCount), - humanize.SI.Uint64(stats.InternalStats.KeyBytes), - humanize.SI.Uint64(stats.InternalStats.ValueBytes), - humanize.SI.Uint64(stats.InternalStats.PointsCoveredByRangeTombstones), + humanize.Count.Uint64(stats.InternalStats.PointCount), + humanize.Bytes.Uint64(stats.InternalStats.KeyBytes), + humanize.Bytes.Uint64(stats.InternalStats.ValueBytes), + humanize.Count.Uint64(stats.InternalStats.PointsCoveredByRangeTombstones), ) if stats.InternalStats.SeparatedPointValue.Count != 0 { s.Printf(", (separated: (count %s, bytes %s, fetched %s)))", - humanize.SI.Uint64(stats.InternalStats.SeparatedPointValue.Count), - humanize.IEC.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytes), - humanize.IEC.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytesFetched)) + humanize.Count.Uint64(stats.InternalStats.SeparatedPointValue.Count), + humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytes), + humanize.Bytes.Uint64(stats.InternalStats.SeparatedPointValue.ValueBytesFetched)) } else { s.Printf(")") } @@ -2741,3 +2828,169 @@ func (stats *IteratorStats) SafeFormat(s redact.SafePrinter, verb rune) { stats.RangeKeyStats.SkippedPoints) } } + +// CanDeterministicallySingleDelete takes a valid iterator and examines internal +// state to determine if a SingleDelete deleting Iterator.Key() would +// deterministically delete the key. CanDeterministicallySingleDelete requires +// the iterator to be oriented in the forward direction (eg, the last +// positioning operation must've been a First, a Seek[Prefix]GE, or a +// Next[Prefix][WithLimit]). +// +// This function does not change the external position of the iterator, and all +// positioning methods should behave the same as if it was never called. This +// function will only return a meaningful result the first time it's invoked at +// an iterator position. This function invalidates the iterator Value's memory, +// and the caller must not rely on the memory safety of the previous Iterator +// position. +// +// If CanDeterministicallySingleDelete returns true AND the key at the iterator +// position is not modified between the creation of the Iterator and the commit +// of a batch containing a SingleDelete over the key, then the caller can be +// assured that SingleDelete is equivalent to Delete on the local engine, but it +// may not be true on another engine that received the same writes and with +// logically equivalent state since this engine may have collapsed multiple SETs +// into one. +func CanDeterministicallySingleDelete(it *Iterator) (bool, error) { + // This function may only be called once per external iterator position. We + // can validate this by checking the last positioning operation. + if it.lastPositioningOp == internalNextOp { + return false, errors.New("pebble: CanDeterministicallySingleDelete called twice") + } + validity, kind := it.internalNext() + var shadowedBySingleDelete bool + for validity == internalNextValid { + switch kind { + case InternalKeyKindDelete, InternalKeyKindDeleteSized: + // A DEL or DELSIZED tombstone is okay. An internal key + // sequence like SINGLEDEL; SET; DEL; SET can be handled + // deterministically. If there are SETs further down, we + // don't care about them. + return true, nil + case InternalKeyKindSingleDelete: + // A SingleDelete is okay as long as when that SingleDelete was + // written, it was written deterministically (eg, with its own + // CanDeterministicallySingleDelete check). Validate that it was + // written deterministically. We'll allow one set to appear after + // the SingleDelete. + shadowedBySingleDelete = true + validity, kind = it.internalNext() + continue + case InternalKeyKindSet, InternalKeyKindSetWithDelete, InternalKeyKindMerge: + // If we observed a single delete, it's allowed to delete 1 key. + // We'll keep looping to validate that the internal keys beneath the + // already-written single delete are copacetic. + if shadowedBySingleDelete { + shadowedBySingleDelete = false + validity, kind = it.internalNext() + continue + } + // We encountered a shadowed SET, SETWITHDEL, MERGE. A SINGLEDEL + // that deleted the KV at the original iterator position could + // result in this key becoming visible. + return false, nil + case InternalKeyKindRangeDelete: + // RangeDeletes are handled by the merging iterator and should never + // be observed by the top-level Iterator. + panic(errors.AssertionFailedf("pebble: unexpected range delete")) + case InternalKeyKindRangeKeySet, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeyDelete: + // Range keys are interleaved at the maximal sequence number and + // should never be observed within a user key. + panic(errors.AssertionFailedf("pebble: unexpected range key")) + default: + panic(errors.AssertionFailedf("pebble: unexpected key kind: %s", errors.Safe(kind))) + } + } + if validity == internalNextError { + return false, it.Error() + } + return true, nil +} + +// internalNextValidity enumerates the potential outcomes of a call to +// internalNext. +type internalNextValidity int8 + +const ( + // internalNextError is returned by internalNext when an error occurred and + // the caller is responsible for checking iter.Error(). + internalNextError internalNextValidity = iota + // internalNextExhausted is returned by internalNext when the next internal + // key is an internal key with a different user key than Iterator.Key(). + internalNextExhausted + // internalNextValid is returned by internalNext when the internal next + // found a shadowed internal key with a user key equal to Iterator.Key(). + internalNextValid +) + +// internalNext advances internal Iterator state forward to expose the +// InternalKeyKind of the next internal key with a user key equal to Key(). +// +// internalNext is a highly specialized operation and is unlikely to be +// generally useful. See Iterator.Next for how to reposition the iterator to the +// next key. internalNext requires the Iterator to be at a valid position in the +// forward direction (the last positioning operation must've been a First, a +// Seek[Prefix]GE, or a Next[Prefix][WithLimit] and Valid() must return true). +// +// internalNext, unlike all other Iterator methods, exposes internal LSM state. +// internalNext advances the Iterator's internal iterator to the next shadowed +// key with a user key equal to Key(). When a key is overwritten or deleted, its +// removal from the LSM occurs lazily as a part of compactions. internalNext +// allows the caller to see whether an obsolete internal key exists with the +// current Key(), and what it's key kind is. Note that the existence of an +// internal key is nondeterministic and dependent on internal LSM state. These +// semantics are unlikely to be applicable to almost all use cases. +// +// If internalNext finds a key that shares the same user key as Key(), it +// returns internalNextValid and the internal key's kind. If internalNext +// encounters an error, it returns internalNextError and the caller is expected +// to call Iterator.Error() to retrieve it. In all other circumstances, +// internalNext returns internalNextExhausted, indicating that there are no more +// additional internal keys with the user key Key(). +// +// internalNext does not change the external position of the iterator, and a +// Next operation should behave the same as if internalNext was never called. +// internalNext does invalidate the iterator Value's memory, and the caller must +// not rely on the memory safety of the previous Iterator position. +func (i *Iterator) internalNext() (internalNextValidity, base.InternalKeyKind) { + i.stats.ForwardStepCount[InterfaceCall]++ + if i.err != nil { + return internalNextError, base.InternalKeyKindInvalid + } else if i.iterValidityState != IterValid { + return internalNextExhausted, base.InternalKeyKindInvalid + } + i.lastPositioningOp = internalNextOp + + switch i.pos { + case iterPosCurForward: + i.iterKey, i.iterValue = i.iter.Next() + if i.iterKey == nil { + // We check i.iter.Error() here and return an internalNextError enum + // variant so that the caller does not need to check i.iter.Error() + // in the common case that the next internal key has a new user key. + if i.err = i.iter.Error(); i.err != nil { + return internalNextError, base.InternalKeyKindInvalid + } + i.pos = iterPosNext + return internalNextExhausted, base.InternalKeyKindInvalid + } else if i.comparer.Equal(i.iterKey.UserKey, i.key) { + return internalNextValid, i.iterKey.Kind() + } + i.pos = iterPosNext + return internalNextExhausted, base.InternalKeyKindInvalid + case iterPosCurReverse, iterPosCurReversePaused, iterPosPrev: + i.err = errors.New("switching from reverse to forward via internalNext is prohibited") + i.iterValidityState = IterExhausted + return internalNextError, base.InternalKeyKindInvalid + case iterPosNext, iterPosCurForwardPaused: + // The previous method already moved onto the next user key. This is + // only possible if + // - the last positioning method was a call to internalNext, and we + // advanced to a new user key. + // - the previous non-internalNext iterator operation encountered a + // range key or merge, forcing an internal Next that found a new + // user key that's not equal to i.Iterator.Key(). + return internalNextExhausted, base.InternalKeyKindInvalid + default: + panic("unreachable") + } +} diff --git a/vendor/github.com/cockroachdb/pebble/level_checker.go b/vendor/github.com/cockroachdb/pebble/level_checker.go index 89f18b0..2901c45 100644 --- a/vendor/github.com/cockroachdb/pebble/level_checker.go +++ b/vendor/github.com/cockroachdb/pebble/level_checker.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "sort" - "sync/atomic" "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" @@ -166,7 +165,7 @@ func (m *simpleMergingIter) step() bool { if m.valueMerger != nil { // Ongoing series of MERGE records. switch item.key.Kind() { - case InternalKeyKindSingleDelete, InternalKeyKindDelete: + case InternalKeyKindSingleDelete, InternalKeyKindDelete, InternalKeyKindDeleteSized: var closer io.Closer _, closer, m.err = m.valueMerger.Finish(true /* includesBase */) if m.err == nil && closer != nil { @@ -346,7 +345,7 @@ func iterateAndCheckTombstones( type checkConfig struct { logger Logger - cmp Compare + comparer *Comparer readState *readState newIters tableNewIters seqNum uint64 @@ -355,6 +354,9 @@ type checkConfig struct { formatKey base.FormatKey } +// cmp is shorthand for comparer.Compare. +func (c *checkConfig) cmp(a, b []byte) int { return c.comparer.Compare(a, b) } + func checkRangeTombstones(c *checkConfig) error { var level int var tombstones []tombstoneWithLevel @@ -380,7 +382,7 @@ func checkRangeTombstones(c *checkConfig) error { atomicUnit, _ := expandToAtomicUnit(c.cmp, lf.Slice(), true /* disableIsCompacting */) lower, upper := manifest.KeyRange(c.cmp, atomicUnit.Iter()) iterToClose, iter, err := c.newIters( - context.Background(), lf.FileMetadata, nil, internalIterOpts{}) + context.Background(), lf.FileMetadata, &IterOptions{level: manifest.Level(lsmLevel)}, internalIterOpts{}) if err != nil { return err } @@ -568,11 +570,11 @@ func (d *DB) CheckLevels(stats *CheckLevelsStats) error { // Determine the seqnum to read at after grabbing the read state (current and // memtables) above. - seqNum := atomic.LoadUint64(&d.mu.versions.atomic.visibleSeqNum) + seqNum := d.mu.versions.visibleSeqNum.Load() checkConfig := &checkConfig{ logger: d.opts.Logger, - cmp: d.cmp, + comparer: d.opts.Comparer, readState: readState, newIters: d.newIters, seqNum: seqNum, @@ -640,7 +642,7 @@ func checkLevelsInternal(c *checkConfig) (err error) { manifestIter := current.L0SublevelFiles[sublevel].Iter() iterOpts := IterOptions{logger: c.logger} li := &levelIter{} - li.init(context.Background(), iterOpts, c.cmp, nil /* split */, c.newIters, manifestIter, + li.init(context.Background(), iterOpts, c.comparer, c.newIters, manifestIter, manifest.L0Sublevel(sublevel), internalIterOpts{}) li.initRangeDel(&mlevelAlloc[0].rangeDelIter) li.initBoundaryContext(&mlevelAlloc[0].levelIterBoundaryContext) @@ -654,7 +656,7 @@ func checkLevelsInternal(c *checkConfig) (err error) { iterOpts := IterOptions{logger: c.logger} li := &levelIter{} - li.init(context.Background(), iterOpts, c.cmp, nil /* split */, c.newIters, + li.init(context.Background(), iterOpts, c.comparer, c.newIters, current.Levels[level].Iter(), manifest.Level(level), internalIterOpts{}) li.initRangeDel(&mlevelAlloc[0].rangeDelIter) li.initBoundaryContext(&mlevelAlloc[0].levelIterBoundaryContext) diff --git a/vendor/github.com/cockroachdb/pebble/level_iter.go b/vendor/github.com/cockroachdb/pebble/level_iter.go index feb809d..ae6b045 100644 --- a/vendor/github.com/cockroachdb/pebble/level_iter.go +++ b/vendor/github.com/cockroachdb/pebble/level_iter.go @@ -36,9 +36,8 @@ type tableNewIters func( // tableNewRangeDelIter takes a tableNewIters and returns a TableNewSpanIter // for the rangedel iterator returned by tableNewIters. func tableNewRangeDelIter(ctx context.Context, newIters tableNewIters) keyspan.TableNewSpanIter { - return func(file *manifest.FileMetadata, iterOptions *keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) { - iter, rangeDelIter, err := newIters( - ctx, file, &IterOptions{RangeKeyFilters: iterOptions.RangeKeyFilters}, internalIterOpts{}) + return func(file *manifest.FileMetadata, iterOptions keyspan.SpanIterOptions) (keyspan.FragmentIterator, error) { + iter, rangeDelIter, err := newIters(ctx, file, nil, internalIterOpts{}) if iter != nil { _ = iter.Close() } @@ -51,6 +50,7 @@ func tableNewRangeDelIter(ctx context.Context, newIters tableNewIters) keyspan.T type internalIterOpts struct { bytesIterated *uint64 + bufferPool *sstable.BufferPool stats *base.InternalIteratorStats boundLimitedFilter sstable.BoundLimitedBlockPropertyFilter } @@ -82,10 +82,11 @@ type levelIter struct { // short-lived (since they pin sstables), (b) plumbing a context into every // method is very painful, (c) they do not (yet) respect context // cancellation and are only used for tracing. - ctx context.Context - logger Logger - cmp Compare - split Split + ctx context.Context + logger Logger + comparer *Comparer + cmp Compare + split Split // The lower/upper bounds for iteration as specified at creation or the most // recent call to SetBounds. lower []byte @@ -200,6 +201,11 @@ type levelIter struct { // cache when constructing new table iterators. internalOpts internalIterOpts + // Scratch space for the obsolete keys filter, when there are no other block + // property filters specified. See the performance note where + // IterOptions.PointKeyFilters is declared. + filtersBuf [1]BlockPropertyFilter + // Disable invariant checks even if they are otherwise enabled. Used by tests // which construct "impossible" situations (e.g. seeking to a key before the // lower bound). @@ -236,24 +242,22 @@ var _ base.InternalIterator = (*levelIter)(nil) // parameter if the caller is never going to call SeekPrefixGE. func newLevelIter( opts IterOptions, - cmp Compare, - split Split, + comparer *Comparer, newIters tableNewIters, files manifest.LevelIterator, level manifest.Level, - bytesIterated *uint64, + internalOpts internalIterOpts, ) *levelIter { l := &levelIter{} - l.init(context.Background(), opts, cmp, split, newIters, files, level, - internalIterOpts{bytesIterated: bytesIterated}) + l.init(context.Background(), opts, comparer, newIters, files, level, + internalOpts) return l } func (l *levelIter) init( ctx context.Context, opts IterOptions, - cmp Compare, - split Split, + comparer *Comparer, newIters tableNewIters, files manifest.LevelIterator, level manifest.Level, @@ -267,10 +271,15 @@ func (l *levelIter) init( l.upper = opts.UpperBound l.tableOpts.TableFilter = opts.TableFilter l.tableOpts.PointKeyFilters = opts.PointKeyFilters + if len(opts.PointKeyFilters) == 0 { + l.tableOpts.PointKeyFilters = l.filtersBuf[:0:1] + } l.tableOpts.UseL6Filters = opts.UseL6Filters l.tableOpts.level = l.level - l.cmp = cmp - l.split = split + l.tableOpts.snapshotForHideObsoletePoints = opts.snapshotForHideObsoletePoints + l.comparer = comparer + l.cmp = comparer.Compare + l.split = comparer.Split l.iterFile = nil l.newIters = newIters l.files = files @@ -788,7 +797,7 @@ func (l *levelIter) SeekPrefixGE( } return l.verify(l.largestBoundary, base.LazyValue{}) } - // It is possible that we are here because bloom filter matching failed. In + // It is possible that we are here because bloom filter matching failed. In // that case it is likely that all keys matching the prefix are wholly // within the current file and cannot be in the subsequent file. In that // case we don't want to go to the next file, since loading and seeking in @@ -796,7 +805,16 @@ func (l *levelIter) SeekPrefixGE( // next file will defeat the optimization for the next SeekPrefixGE that is // called with flags.TrySeekUsingNext(), since for sparse key spaces it is // likely that the next key will also be contained in the current file. - if n := l.split(l.iterFile.LargestPointKey.UserKey); l.cmp(prefix, l.iterFile.LargestPointKey.UserKey[:n]) < 0 { + var n int + if l.split != nil { + // If the split function is specified, calculate the prefix length accordingly. + n = l.split(l.iterFile.LargestPointKey.UserKey) + } else { + // If the split function is not specified, the entire key is used as the + // prefix. This case can occur when getIter uses SeekPrefixGE. + n = len(l.iterFile.LargestPointKey.UserKey) + } + if l.cmp(prefix, l.iterFile.LargestPointKey.UserKey[:n]) < 0 { return nil, base.LazyValue{} } return l.verify(l.skipEmptyFileForward()) @@ -1066,7 +1084,9 @@ func (l *levelIter) skipEmptyFileForward() (*InternalKey, base.LazyValue) { if *l.rangeDelIterPtr != nil && l.filteredIter != nil && l.filteredIter.MaybeFilteredKeys() { l.largestBoundary = &l.iterFile.Largest - l.boundaryContext.isIgnorableBoundaryKey = true + if l.boundaryContext != nil { + l.boundaryContext.isIgnorableBoundaryKey = true + } return l.largestBoundary, base.LazyValue{} } } @@ -1155,7 +1175,9 @@ func (l *levelIter) skipEmptyFileBackward() (*InternalKey, base.LazyValue) { // the next file. if *l.rangeDelIterPtr != nil && l.filteredIter != nil && l.filteredIter.MaybeFilteredKeys() { l.smallestBoundary = &l.iterFile.Smallest - l.boundaryContext.isIgnorableBoundaryKey = true + if l.boundaryContext != nil { + l.boundaryContext.isIgnorableBoundaryKey = true + } return l.smallestBoundary, base.LazyValue{} } } diff --git a/vendor/github.com/cockroachdb/pebble/log_recycler.go b/vendor/github.com/cockroachdb/pebble/log_recycler.go index 85e64cc..c8c2ff4 100644 --- a/vendor/github.com/cockroachdb/pebble/log_recycler.go +++ b/vendor/github.com/cockroachdb/pebble/log_recycler.go @@ -32,14 +32,14 @@ type logRecycler struct { // the log file should not be deleted (i.e. the log is being recycled), and // false otherwise. func (r *logRecycler) add(logInfo fileInfo) bool { - if logInfo.fileNum < r.minRecycleLogNum { + if logInfo.fileNum.FileNum() < r.minRecycleLogNum { return false } r.mu.Lock() defer r.mu.Unlock() - if logInfo.fileNum <= r.mu.maxLogNum { + if logInfo.fileNum.FileNum() <= r.mu.maxLogNum { // The log file number was already considered for recycling. Don't consider // it again. This avoids a race between adding the same log file for // recycling multiple times, and removing the log file for actual @@ -49,7 +49,7 @@ func (r *logRecycler) add(logInfo fileInfo) bool { // shouldn't be deleted. return true } - r.mu.maxLogNum = logInfo.fileNum + r.mu.maxLogNum = logInfo.fileNum.FileNum() if len(r.mu.logs) >= r.limit { return false } @@ -89,7 +89,7 @@ func (r *logRecycler) pop(logNum FileNum) error { if len(r.mu.logs) == 0 { return errors.New("pebble: log recycler empty") } - if r.mu.logs[0].fileNum != logNum { + if r.mu.logs[0].fileNum.FileNum() != logNum { return errors.Errorf("pebble: log recycler invalid %d vs %d", errors.Safe(logNum), errors.Safe(fileInfoNums(r.mu.logs))) } r.mu.logs = r.mu.logs[1:] @@ -102,7 +102,7 @@ func fileInfoNums(finfos []fileInfo) []FileNum { } nums := make([]FileNum, len(finfos)) for i := range finfos { - nums[i] = finfos[i].fileNum + nums[i] = finfos[i].fileNum.FileNum() } return nums } diff --git a/vendor/github.com/cockroachdb/pebble/mem_table.go b/vendor/github.com/cockroachdb/pebble/mem_table.go index 5691102..e728e94 100644 --- a/vendor/github.com/cockroachdb/pebble/mem_table.go +++ b/vendor/github.com/cockroachdb/pebble/mem_table.go @@ -10,12 +10,12 @@ import ( "os" "sync" "sync/atomic" - "unsafe" "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/arenaskl" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manual" "github.com/cockroachdb/pebble/internal/rangedel" "github.com/cockroachdb/pebble/internal/rangekey" ) @@ -80,12 +80,21 @@ type memTable struct { // inflight mutations that have reserved space in the memtable but not yet // applied. The memtable cannot be flushed to disk until the writer refs // drops to zero. - writerRefs int32 + writerRefs atomic.Int32 tombstones keySpanCache rangeKeys keySpanCache // The current logSeqNum at the time the memtable was created. This is // guaranteed to be less than or equal to any seqnum stored in the memtable. - logSeqNum uint64 + logSeqNum uint64 + releaseAccountingReservation func() +} + +func (m *memTable) free() { + if m != nil { + m.releaseAccountingReservation() + manual.Free(m.arenaBuf) + m.arenaBuf = nil + } } // memTableOptions holds configuration used when creating a memTable. All of @@ -93,9 +102,10 @@ type memTable struct { // which is used by tests. type memTableOptions struct { *Options - arenaBuf []byte - size int - logSeqNum uint64 + arenaBuf []byte + size int + logSeqNum uint64 + releaseAccountingReservation func() } func checkMemTable(obj interface{}) { @@ -110,18 +120,24 @@ func checkMemTable(obj interface{}) { // Options.MemTableSize is used instead. func newMemTable(opts memTableOptions) *memTable { opts.Options = opts.Options.EnsureDefaults() - if opts.size == 0 { - opts.size = opts.MemTableSize - } + m := new(memTable) + m.init(opts) + return m +} - m := &memTable{ - cmp: opts.Comparer.Compare, - formatKey: opts.Comparer.FormatKey, - equal: opts.Comparer.Equal, - arenaBuf: opts.arenaBuf, - writerRefs: 1, - logSeqNum: opts.logSeqNum, +func (m *memTable) init(opts memTableOptions) { + if opts.size == 0 { + opts.size = int(opts.MemTableSize) } + *m = memTable{ + cmp: opts.Comparer.Compare, + formatKey: opts.Comparer.FormatKey, + equal: opts.Comparer.Equal, + arenaBuf: opts.arenaBuf, + logSeqNum: opts.logSeqNum, + releaseAccountingReservation: opts.releaseAccountingReservation, + } + m.writerRefs.Store(1) m.tombstones = keySpanCache{ cmp: m.cmp, formatKey: m.formatKey, @@ -144,18 +160,18 @@ func newMemTable(opts memTableOptions) *memTable { m.rangeDelSkl.Reset(arena, m.cmp) m.rangeKeySkl.Reset(arena, m.cmp) m.reserved = arena.Size() - return m } func (m *memTable) writerRef() { - switch v := atomic.AddInt32(&m.writerRefs, 1); { + switch v := m.writerRefs.Add(1); { case v <= 1: panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) } } -func (m *memTable) writerUnref() bool { - switch v := atomic.AddInt32(&m.writerRefs, -1); { +// writerUnref drops a ref on the memtable. Returns true if this was the last ref. +func (m *memTable) writerUnref() (wasLastRef bool) { + switch v := m.writerRefs.Add(-1); { case v < 0: panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) case v == 0: @@ -165,8 +181,9 @@ func (m *memTable) writerUnref() bool { } } +// readyForFlush is part of the flushable interface. func (m *memTable) readyForFlush() bool { - return atomic.LoadInt32(&m.writerRefs) == 0 + return m.writerRefs.Load() == 0 } // Prepare reserves space for the batch in the memtable and references the @@ -194,11 +211,13 @@ func (m *memTable) apply(batch *Batch, seqNum uint64) error { var tombstoneCount, rangeKeyCount uint32 startSeqNum := seqNum for r := batch.Reader(); ; seqNum++ { - kind, ukey, value, ok := r.Next() + kind, ukey, value, ok, err := r.Next() if !ok { + if err != nil { + return err + } break } - var err error ikey := base.MakeInternalKey(ukey, seqNum, kind) switch kind { case InternalKeyKindRangeDelete: @@ -233,17 +252,19 @@ func (m *memTable) apply(batch *Batch, seqNum uint64) error { return nil } -// newIter returns an iterator that is unpositioned (Iterator.Valid() will -// return false). The iterator can be positioned via a call to SeekGE, -// SeekLT, First or Last. +// newIter is part of the flushable interface. It returns an iterator that is +// unpositioned (Iterator.Valid() will return false). The iterator can be +// positioned via a call to SeekGE, SeekLT, First or Last. func (m *memTable) newIter(o *IterOptions) internalIterator { return m.skl.NewIter(o.GetLowerBound(), o.GetUpperBound()) } +// newFlushIter is part of the flushable interface. func (m *memTable) newFlushIter(o *IterOptions, bytesFlushed *uint64) internalIterator { return m.skl.NewFlushIter(bytesFlushed) } +// newRangeDelIter is part of the flushable interface. func (m *memTable) newRangeDelIter(*IterOptions) keyspan.FragmentIterator { tombstones := m.tombstones.get() if tombstones == nil { @@ -252,6 +273,7 @@ func (m *memTable) newRangeDelIter(*IterOptions) keyspan.FragmentIterator { return keyspan.NewIter(m.cmp, tombstones) } +// newRangeKeyIter is part of the flushable interface. func (m *memTable) newRangeKeyIter(*IterOptions) keyspan.FragmentIterator { rangeKeys := m.rangeKeys.get() if rangeKeys == nil { @@ -260,13 +282,14 @@ func (m *memTable) newRangeKeyIter(*IterOptions) keyspan.FragmentIterator { return keyspan.NewIter(m.cmp, rangeKeys) } +// containsRangeKeys is part of the flushable interface. func (m *memTable) containsRangeKeys() bool { - return atomic.LoadUint32(&m.rangeKeys.atomicCount) > 0 + return m.rangeKeys.count.Load() > 0 } func (m *memTable) availBytes() uint32 { a := m.skl.Arena() - if atomic.LoadInt32(&m.writerRefs) == 1 { + if m.writerRefs.Load() == 1 { // If there are no other concurrent apply operations, we can update the // reserved bytes setting to accurately reflect how many bytes of been // allocated vs the over-estimation present in memTableEntrySize. @@ -275,10 +298,12 @@ func (m *memTable) availBytes() uint32 { return a.Capacity() - m.reserved } +// inuseBytes is part of the flushable interface. func (m *memTable) inuseBytes() uint64 { return uint64(m.skl.Size() - memTableEmptySize) } +// totalBytes is part of the flushable interface. func (m *memTable) totalBytes() uint64 { return uint64(m.skl.Arena().Capacity()) } @@ -355,8 +380,8 @@ func (f *keySpanFrags) get( // invalidated whenever a key of the same kind is added to a memTable, and // populated when empty when a span iterator of that key kind is created. type keySpanCache struct { - atomicCount uint32 - frags unsafe.Pointer + count atomic.Uint32 + frags atomic.Pointer[keySpanFrags] cmp Compare formatKey base.FormatKey constructSpan constructSpan @@ -366,23 +391,20 @@ type keySpanCache struct { // Invalidate the current set of cached spans, indicating the number of // spans that were added. func (c *keySpanCache) invalidate(count uint32) { - newCount := atomic.AddUint32(&c.atomicCount, count) + newCount := c.count.Add(count) var frags *keySpanFrags for { - oldPtr := atomic.LoadPointer(&c.frags) - if oldPtr != nil { - oldFrags := (*keySpanFrags)(oldPtr) - if oldFrags.count >= newCount { - // Someone else invalidated the cache before us and their invalidation - // subsumes ours. - break - } + oldFrags := c.frags.Load() + if oldFrags != nil && oldFrags.count >= newCount { + // Someone else invalidated the cache before us and their invalidation + // subsumes ours. + break } if frags == nil { frags = &keySpanFrags{count: newCount} } - if atomic.CompareAndSwapPointer(&c.frags, oldPtr, unsafe.Pointer(frags)) { + if c.frags.CompareAndSwap(oldFrags, frags) { // We successfully invalidated the cache. break } @@ -391,7 +413,7 @@ func (c *keySpanCache) invalidate(count uint32) { } func (c *keySpanCache) get() []keyspan.Span { - frags := (*keySpanFrags)(atomic.LoadPointer(&c.frags)) + frags := c.frags.Load() if frags == nil { return nil } diff --git a/vendor/github.com/cockroachdb/pebble/merging_iter.go b/vendor/github.com/cockroachdb/pebble/merging_iter.go index 4e2426d..a076686 100644 --- a/vendor/github.com/cockroachdb/pebble/merging_iter.go +++ b/vendor/github.com/cockroachdb/pebble/merging_iter.go @@ -27,6 +27,10 @@ type mergingIterLevel struct { // iterKey and iterValue cache the current key and value iter are pointed at. iterKey *InternalKey iterValue base.LazyValue + // levelIter is non-nil if this level's iter is ultimately backed by a + // *levelIter. The handle in iter may have wrapped the levelIter with + // intermediary internalIterator implementations. + levelIter *levelIter // levelIterBoundaryContext's fields are set when using levelIter, in order // to surface sstable boundary keys and file-level context. See levelIter @@ -1371,9 +1375,9 @@ func (m *mergingIter) DebugString() string { } func (m *mergingIter) ForEachLevelIter(fn func(li *levelIter) bool) { - for _, iter := range m.levels { - if li, ok := iter.iter.(*levelIter); ok { - if done := fn(li); done { + for _, ml := range m.levels { + if ml.levelIter != nil { + if done := fn(ml.levelIter); done { break } } diff --git a/vendor/github.com/cockroachdb/pebble/metrics.go b/vendor/github.com/cockroachdb/pebble/metrics.go index 425b907..cab9291 100644 --- a/vendor/github.com/cockroachdb/pebble/metrics.go +++ b/vendor/github.com/cockroachdb/pebble/metrics.go @@ -6,12 +6,13 @@ package pebble import ( "fmt" - "strings" + "math" "time" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache" "github.com/cockroachdb/pebble/record" "github.com/cockroachdb/pebble/sstable" "github.com/cockroachdb/redact" @@ -28,13 +29,10 @@ type FilterMetrics = sstable.FilterMetrics // comment in base. type ThroughputMetric = base.ThroughputMetric -func formatCacheMetrics(w redact.SafePrinter, m *CacheMetrics, name redact.SafeString) { - w.Printf("%7s %9s %7s %6.1f%% (score == hit-rate)\n", - name, - humanize.SI.Int64(m.Count), - humanize.IEC.Int64(m.Size), - redact.Safe(hitRate(m.Hits, m.Misses))) -} +// SecondaryCacheMetrics holds metrics for the persistent secondary cache +// that caches commonly accessed blocks from blob storage on a local +// file system. +type SecondaryCacheMetrics = sharedcache.Metrics // LevelMetrics holds per-level metrics such as the number of files and total // size of the files, and compaction related metrics. @@ -46,9 +44,14 @@ type LevelMetrics struct { Sublevels int32 // The total number of files in the level. NumFiles int64 + // The total number of virtual sstables in the level. + NumVirtualFiles uint64 // The total size in bytes of the files in the level. Size int64 - // The level's compaction score. + // The total size of the virtual sstables in the level. + VirtualSize uint64 + // The level's compaction score. This is the compensatedScoreRatio in the + // candidateLevelInfo. Score float64 // The number of incoming bytes from other levels read during // compactions. This excludes bytes moved and bytes ingested. For L0 this is @@ -79,6 +82,18 @@ type LevelMetrics struct { TablesIngested uint64 // The number of sstables moved to this level by a "move" compaction. TablesMoved uint64 + + MultiLevel struct { + // BytesInTop are the total bytes in a multilevel compaction coming from the top level. + BytesInTop uint64 + + // BytesIn, exclusively for multiLevel compactions. + BytesIn uint64 + + // BytesRead, exclusively for multilevel compactions. + BytesRead uint64 + } + // Additional contains misc additional metrics that are not always printed. Additional struct { // The sum of Properties.ValueBlocksSize for all the sstables in this @@ -96,6 +111,8 @@ type LevelMetrics struct { // Add updates the counter metrics for the level. func (m *LevelMetrics) Add(u *LevelMetrics) { m.NumFiles += u.NumFiles + m.NumVirtualFiles += u.NumVirtualFiles + m.VirtualSize += u.VirtualSize m.Size += u.Size m.BytesIn += u.BytesIn m.BytesIngested += u.BytesIngested @@ -107,6 +124,9 @@ func (m *LevelMetrics) Add(u *LevelMetrics) { m.TablesFlushed += u.TablesFlushed m.TablesIngested += u.TablesIngested m.TablesMoved += u.TablesMoved + m.MultiLevel.BytesInTop += u.MultiLevel.BytesInTop + m.MultiLevel.BytesRead += u.MultiLevel.BytesRead + m.MultiLevel.BytesIn += u.MultiLevel.BytesIn m.Additional.BytesWrittenDataBlocks += u.Additional.BytesWrittenDataBlocks m.Additional.BytesWrittenValueBlocks += u.Additional.BytesWrittenValueBlocks m.Additional.ValueBlocksSize += u.Additional.ValueBlocksSize @@ -121,32 +141,6 @@ func (m *LevelMetrics) WriteAmp() float64 { return float64(m.BytesFlushed+m.BytesCompacted) / float64(m.BytesIn) } -// format generates a string of the receiver's metrics, formatting it into the -// supplied buffer. -func (m *LevelMetrics) format( - w redact.SafePrinter, score redact.SafeValue, includeValueBlocksSize bool, -) { - w.Printf("%9d %7s %7s %7s %7s %7s %7s %7s %7s %7s %7s %7d %7.1f", - redact.Safe(m.NumFiles), - humanize.IEC.Int64(m.Size), - score, - humanize.IEC.Uint64(m.BytesIn), - humanize.IEC.Uint64(m.BytesIngested), - humanize.SI.Uint64(m.TablesIngested), - humanize.IEC.Uint64(m.BytesMoved), - humanize.SI.Uint64(m.TablesMoved), - humanize.IEC.Uint64(m.BytesFlushed+m.BytesCompacted), - humanize.SI.Uint64(m.TablesFlushed+m.TablesCompacted), - humanize.IEC.Uint64(m.BytesRead), - redact.Safe(m.Sublevels), - redact.Safe(m.WriteAmp())) - if includeValueBlocksSize { - w.Printf(" %7s\n", humanize.IEC.Uint64(m.Additional.ValueBlocksSize)) - } else { - w.SafeString("\n") - } -} - // Metrics holds metrics for various subsystems of the DB such as the Cache, // Compactions, WAL, and per-Level metrics. // @@ -158,14 +152,15 @@ type Metrics struct { Compact struct { // The total number of compactions, and per-compaction type counts. - Count int64 - DefaultCount int64 - DeleteOnlyCount int64 - ElisionOnlyCount int64 - MoveCount int64 - ReadCount int64 - RewriteCount int64 - MultiLevelCount int64 + Count int64 + DefaultCount int64 + DeleteOnlyCount int64 + ElisionOnlyCount int64 + MoveCount int64 + ReadCount int64 + RewriteCount int64 + MultiLevelCount int64 + CounterLevelCount int64 // An estimate of the number of bytes that need to be compacted for the LSM // to reach a stable state. EstimatedDebt uint64 @@ -179,6 +174,14 @@ type Metrics struct { // compaction. Such files are compacted in a rewrite compaction // when no other compactions are picked. MarkedFiles int + // Duration records the cumulative duration of all compactions since the + // database was opened. + Duration time.Duration + } + + Ingest struct { + // The total number of ingestions + Count uint64 } Flush struct { @@ -210,7 +213,10 @@ type Metrics struct { // The count of memtables. Count int64 // The number of bytes present in zombie memtables which are no longer - // referenced by the current DB state but are still in use by an iterator. + // referenced by the current DB state. An unbounded number of memtables + // may be zombie if they're still in use by an iterator. One additional + // memtable may be zombie if it's no longer in use and waiting to be + // recycled. ZombieSize uint64 // The count of zombie memtables. ZombieCount int64 @@ -222,6 +228,9 @@ type Metrics struct { // The approximate count of internal tombstones (DEL, SINGLEDEL and // RANGEDEL key kinds) within the database. TombstoneCount uint64 + // A cumulative total number of missized DELSIZED keys encountered by + // compactions since the database was opened. + MissizedTombstonesCount uint64 } Snapshots struct { @@ -250,12 +259,18 @@ type Metrics struct { ZombieSize uint64 // The count of zombie tables. ZombieCount int64 + // The count of the backing sstables. + BackingTableCount uint64 + // The sum of the sizes of the all of the backing sstables. + BackingTableSize uint64 } TableCache CacheMetrics // Count of the number of open sstable iterators. TableIters int64 + // Uptime is the total time since this DB was opened. + Uptime time.Duration WAL struct { // Number of live WAL files. @@ -281,6 +296,8 @@ type Metrics struct { record.LogWriterMetrics } + SecondaryCacheMetrics SecondaryCacheMetrics + private struct { optionsFileSize uint64 manifestFileSize uint64 @@ -294,6 +311,13 @@ var ( prometheus.LinearBuckets(0.0, float64(time.Microsecond*100), 50), prometheus.ExponentialBucketsRange(float64(time.Millisecond*5), float64(10*time.Second), 50)..., ) + + // SecondaryCacheIOBuckets exported to enable exporting from package pebble to + // enable exporting metrics with below buckets in CRDB. + SecondaryCacheIOBuckets = sharedcache.IOBuckets + // SecondaryCacheChannelWriteBuckets exported to enable exporting from package + // pebble to enable exporting metrics with below buckets in CRDB. + SecondaryCacheChannelWriteBuckets = sharedcache.ChannelWriteBuckets ) // DiskSpaceUsage returns the total disk space used by the database in bytes, @@ -313,12 +337,25 @@ func (m *Metrics) DiskSpaceUsage() uint64 { return usageBytes } -func (m *Metrics) levelSizes() [numLevels]int64 { - var sizes [numLevels]int64 - for i := 0; i < len(sizes); i++ { - sizes[i] = m.Levels[i].Size +// NumVirtual is the number of virtual sstables in the latest version +// summed over every level in the lsm. +func (m *Metrics) NumVirtual() uint64 { + var n uint64 + for _, level := range m.Levels { + n += level.NumVirtualFiles } - return sizes + return n +} + +// VirtualSize is the sum of the sizes of the virtual sstables in the +// latest version. BackingTableSize - VirtualSize gives an estimate for +// the space amplification caused by not compacting virtual sstables. +func (m *Metrics) VirtualSize() uint64 { + var size uint64 + for _, level := range m.Levels { + size += level.VirtualSize + } + return size } // ReadAmp returns the current read amplification of the database. @@ -349,59 +386,34 @@ func (m *Metrics) Total() LevelMetrics { return total } -const notApplicable = redact.SafeString("-") - -func (m *Metrics) formatWAL(w redact.SafePrinter) { - var writeAmp float64 - if m.WAL.BytesIn > 0 { - writeAmp = float64(m.WAL.BytesWritten) / float64(m.WAL.BytesIn) - } - w.Printf(" WAL %9d %7s %7s %7s %7s %7s %7s %7s %7s %7s %7s %7s %7.1f\n", - redact.Safe(m.WAL.Files), - humanize.Uint64(m.WAL.Size), - notApplicable, - humanize.Uint64(m.WAL.BytesIn), - notApplicable, - notApplicable, - notApplicable, - notApplicable, - humanize.Uint64(m.WAL.BytesWritten), - notApplicable, - notApplicable, - notApplicable, - redact.Safe(writeAmp)) -} - -// String pretty-prints the metrics, showing a line for the WAL, a line per-level, and -// a total: +// String pretty-prints the metrics as below: // -// __level_____count____size___score______in__ingest(sz_cnt)____move(sz_cnt)___write(sz_cnt)____read___r-amp___w-amp -// WAL 1 28 B - 17 B - - - - 56 B - - - 3.3 -// 0 1 770 B 0.25 28 B 0 B 0 0 B 0 770 B 1 0 B 1 27.5 -// 1 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 -// 2 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 -// 3 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 -// 4 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 -// 5 0 0 B 0.00 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 -// 6 0 0 B - 0 B 0 B 0 0 B 0 0 B 0 0 B 0 0.0 -// total 1 770 B - 56 B 0 B 0 0 B 0 826 B 1 0 B 1 14.8 -// flush 1 0 B 0 0 (ingest = ingested-as-flushable, move = tables-ingested) -// compact 0 0 B 0 B 0 (size == estimated-debt, score = in-progress-bytes, in = num-in-progress) -// ctype 0 0 0 0 0 0 0 (default, delete, elision, move, read, rewrite, multi-level) -// memtbl 1 256 K -// zmemtbl 1 256 K -// ztbl 0 0 B -// bcache 4 697 B 0.0% (score == hit-rate) -// tcache 1 696 B 0.0% (score == hit-rate) -// snaps 0 - 0 (score == earliest seq num) -// titers 1 -// filter - - 0.0% (score == utility) -// -// The WAL "in" metric is the size of the batches written to the WAL. The WAL -// "write" metric is the size of the physical data written to the WAL which -// includes record fragment overhead. Write amplification is computed as -// bytes-written / bytes-in, except for the total row where bytes-in is -// replaced with WAL-bytes-written + bytes-ingested. +// | | | | ingested | moved | written | | amp +// level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w +// ------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+--------- +// 0 | 101 102B 0B 0 | 103.0 | 104B | 112 104B | 113 106B | 221 217B | 107B | 1 2.1 +// 1 | 201 202B 0B 0 | 203.0 | 204B | 212 204B | 213 206B | 421 417B | 207B | 2 2.0 +// 2 | 301 302B 0B 0 | 303.0 | 304B | 312 304B | 313 306B | 621 617B | 307B | 3 2.0 +// 3 | 401 402B 0B 0 | 403.0 | 404B | 412 404B | 413 406B | 821 817B | 407B | 4 2.0 +// 4 | 501 502B 0B 0 | 503.0 | 504B | 512 504B | 513 506B | 1.0K 1017B | 507B | 5 2.0 +// 5 | 601 602B 0B 0 | 603.0 | 604B | 612 604B | 613 606B | 1.2K 1.2KB | 607B | 6 2.0 +// 6 | 701 702B 0B 0 | - | 704B | 712 704B | 713 706B | 1.4K 1.4KB | 707B | 7 2.0 +// total | 2.8K 2.7KB 0B 0 | - | 2.8KB | 2.9K 2.8KB | 2.9K 2.8KB | 5.7K 8.4KB | 2.8KB | 28 3.0 +// ------------------------------------------------------------------------------------------------------------------- +// WAL: 22 files (24B) in: 25B written: 26B (4% overhead) +// Flushes: 8 +// Compactions: 5 estimated debt: 6B in progress: 2 (7B) +// default: 27 delete: 28 elision: 29 move: 30 read: 31 rewrite: 32 multi-level: 33 +// MemTables: 12 (11B) zombie: 14 (13B) +// Zombie tables: 16 (15B) +// Backing tables: 0 (0B) +// Block cache: 2 entries (1B) hit rate: 42.9% +// Table cache: 18 entries (17B) hit rate: 48.7% +// Secondary cache: 40 entries (40B) hit rate: 49.9% +// Snapshots: 4 earliest seq num: 1024 +// Table iters: 21 +// Filter utility: 47.4% +// Ingestions: 27 as flushable: 36 (34B in 35 tables) func (m *Metrics) String() string { return redact.StringWithoutMarkers(m) } @@ -422,29 +434,84 @@ func (m *Metrics) SafeFormat(w redact.SafePrinter, _ rune) { // width specifiers. When the issue is fixed, we can convert these to // RedactableStrings. https://github.com/cockroachdb/redact/issues/17 - haveValueBlocks := false - var valueBlocksHeading redact.SafeString - for level := 0; level < numLevels; level++ { - if m.Levels[level].Additional.ValueBlocksSize > 0 { - haveValueBlocks = true - valueBlocksHeading = "__val-bl" - break + multiExists := m.Compact.MultiLevelCount > 0 + appendIfMulti := func(line redact.SafeString) { + if multiExists { + w.SafeString(line) } } + newline := func() { + w.SafeString("\n") + } + + w.SafeString(" | | | | ingested | moved | written | | amp") + appendIfMulti(" | multilevel") + newline() + w.SafeString("level | tables size val-bl vtables | score | in | tables size | tables size | tables size | read | r w") + appendIfMulti(" | top in read") + newline() + w.SafeString("------+-----------------------------+-------+-------+--------------+--------------+--------------+-------+---------") + appendIfMulti("-+------------------") + newline() + + // formatRow prints out a row of the table. + formatRow := func(m *LevelMetrics, score float64) { + scoreStr := "-" + if !math.IsNaN(score) { + // Try to keep the string no longer than 5 characters. + switch { + case score < 99.995: + scoreStr = fmt.Sprintf("%.2f", score) + case score < 999.95: + scoreStr = fmt.Sprintf("%.1f", score) + default: + scoreStr = fmt.Sprintf("%.0f", score) + } + } + var wampStr string + if wamp := m.WriteAmp(); wamp > 99.5 { + wampStr = fmt.Sprintf("%.0f", wamp) + } else { + wampStr = fmt.Sprintf("%.1f", wamp) + } + + w.Printf("| %5s %6s %6s %7s | %5s | %5s | %5s %6s | %5s %6s | %5s %6s | %5s | %3d %4s", + humanize.Count.Int64(m.NumFiles), + humanize.Bytes.Int64(m.Size), + humanize.Bytes.Uint64(m.Additional.ValueBlocksSize), + humanize.Count.Uint64(m.NumVirtualFiles), + redact.Safe(scoreStr), + humanize.Bytes.Uint64(m.BytesIn), + humanize.Count.Uint64(m.TablesIngested), + humanize.Bytes.Uint64(m.BytesIngested), + humanize.Count.Uint64(m.TablesMoved), + humanize.Bytes.Uint64(m.BytesMoved), + humanize.Count.Uint64(m.TablesFlushed+m.TablesCompacted), + humanize.Bytes.Uint64(m.BytesFlushed+m.BytesCompacted), + humanize.Bytes.Uint64(m.BytesRead), + redact.Safe(m.Sublevels), + redact.Safe(wampStr)) + + if multiExists { + w.Printf(" | %5s %5s %5s", + humanize.Bytes.Uint64(m.MultiLevel.BytesInTop), + humanize.Bytes.Uint64(m.MultiLevel.BytesIn), + humanize.Bytes.Uint64(m.MultiLevel.BytesRead)) + } + newline() + } + var total LevelMetrics - w.Printf("__level_____count____size___score______in__ingest(sz_cnt)"+ - "____move(sz_cnt)___write(sz_cnt)____read___r-amp___w-amp%s\n", valueBlocksHeading) - m.formatWAL(w) for level := 0; level < numLevels; level++ { l := &m.Levels[level] - w.Printf("%7d ", redact.Safe(level)) + w.Printf("%5d ", redact.Safe(level)) // Format the score. - var score redact.SafeValue = notApplicable + score := math.NaN() if level < numLevels-1 { - score = redact.Safe(fmt.Sprintf("%0.2f", l.Score)) + score = l.Score } - l.format(w, score, haveValueBlocks) + formatRow(l, score) total.Add(l) total.Sublevels += l.Sublevels } @@ -454,57 +521,105 @@ func (m *Metrics) SafeFormat(w redact.SafePrinter, _ rune) { // the bytes written to the log and bytes written externally and then // ingested. total.BytesFlushed += total.BytesIn - w.SafeString(" total ") - total.format(w, notApplicable, haveValueBlocks) + w.SafeString("total ") + formatRow(&total, math.NaN()) - w.Printf(" flush %9d %31s %7d %7d %s\n", - redact.Safe(m.Flush.Count), - humanize.IEC.Uint64(m.Flush.AsIngestBytes), - redact.Safe(m.Flush.AsIngestTableCount), - redact.Safe(m.Flush.AsIngestCount), - redact.SafeString(`(ingest = tables-ingested, move = ingested-as-flushable)`)) - w.Printf("compact %9d %7s %7s %7d %s %s\n", + w.SafeString("-------------------------------------------------------------------------------------------------------------------") + appendIfMulti("--------------------") + newline() + w.Printf("WAL: %d files (%s) in: %s written: %s (%.0f%% overhead)\n", + redact.Safe(m.WAL.Files), + humanize.Bytes.Uint64(m.WAL.Size), + humanize.Bytes.Uint64(m.WAL.BytesIn), + humanize.Bytes.Uint64(m.WAL.BytesWritten), + redact.Safe(percent(int64(m.WAL.BytesWritten)-int64(m.WAL.BytesIn), int64(m.WAL.BytesIn)))) + + w.Printf("Flushes: %d\n", redact.Safe(m.Flush.Count)) + + w.Printf("Compactions: %d estimated debt: %s in progress: %d (%s)\n", redact.Safe(m.Compact.Count), - humanize.IEC.Uint64(m.Compact.EstimatedDebt), - humanize.IEC.Int64(m.Compact.InProgressBytes), + humanize.Bytes.Uint64(m.Compact.EstimatedDebt), redact.Safe(m.Compact.NumInProgress), - redact.SafeString(strings.Repeat(" ", 24)), - redact.SafeString(`(size == estimated-debt, score = in-progress-bytes, in = num-in-progress)`)) - w.Printf(" ctype %9d %7d %7d %7d %7d %7d %7d %s\n", + humanize.Bytes.Int64(m.Compact.InProgressBytes)) + + w.Printf(" default: %d delete: %d elision: %d move: %d read: %d rewrite: %d multi-level: %d\n", redact.Safe(m.Compact.DefaultCount), redact.Safe(m.Compact.DeleteOnlyCount), redact.Safe(m.Compact.ElisionOnlyCount), redact.Safe(m.Compact.MoveCount), redact.Safe(m.Compact.ReadCount), redact.Safe(m.Compact.RewriteCount), - redact.Safe(m.Compact.MultiLevelCount), - redact.SafeString(`(default, delete, elision, move, read, rewrite, multi-level)`)) - w.Printf(" memtbl %9d %7s\n", + redact.Safe(m.Compact.MultiLevelCount)) + + w.Printf("MemTables: %d (%s) zombie: %d (%s)\n", redact.Safe(m.MemTable.Count), - humanize.IEC.Uint64(m.MemTable.Size)) - w.Printf("zmemtbl %9d %7s\n", + humanize.Bytes.Uint64(m.MemTable.Size), redact.Safe(m.MemTable.ZombieCount), - humanize.IEC.Uint64(m.MemTable.ZombieSize)) - w.Printf(" ztbl %9d %7s\n", + humanize.Bytes.Uint64(m.MemTable.ZombieSize)) + + w.Printf("Zombie tables: %d (%s)\n", redact.Safe(m.Table.ZombieCount), - humanize.IEC.Uint64(m.Table.ZombieSize)) - formatCacheMetrics(w, &m.BlockCache, "bcache") - formatCacheMetrics(w, &m.TableCache, "tcache") - w.Printf(" snaps %9d %7s %7d (score == earliest seq num)\n", + humanize.Bytes.Uint64(m.Table.ZombieSize)) + + w.Printf("Backing tables: %d (%s)\n", + redact.Safe(m.Table.BackingTableCount), + humanize.Bytes.Uint64(m.Table.BackingTableSize)) + w.Printf("Virtual tables: %d (%s)\n", + redact.Safe(m.NumVirtual()), + humanize.Bytes.Uint64(m.VirtualSize())) + + formatCacheMetrics := func(m *CacheMetrics, name redact.SafeString) { + w.Printf("%s: %s entries (%s) hit rate: %.1f%%\n", + name, + humanize.Count.Int64(m.Count), + humanize.Bytes.Int64(m.Size), + redact.Safe(hitRate(m.Hits, m.Misses))) + } + formatCacheMetrics(&m.BlockCache, "Block cache") + formatCacheMetrics(&m.TableCache, "Table cache") + + formatSharedCacheMetrics := func(w redact.SafePrinter, m *SecondaryCacheMetrics, name redact.SafeString) { + w.Printf("%s: %s entries (%s) hit rate: %.1f%%\n", + name, + humanize.Count.Int64(m.Count), + humanize.Bytes.Int64(m.Size), + redact.Safe(hitRate(m.ReadsWithFullHit, m.ReadsWithPartialHit+m.ReadsWithNoHit))) + } + formatSharedCacheMetrics(w, &m.SecondaryCacheMetrics, "Secondary cache") + + w.Printf("Snapshots: %d earliest seq num: %d\n", redact.Safe(m.Snapshots.Count), - notApplicable, redact.Safe(m.Snapshots.EarliestSeqNum)) - w.Printf(" titers %9d\n", redact.Safe(m.TableIters)) - w.Printf(" filter %9s %7s %6.1f%% (score == utility)\n", - notApplicable, - notApplicable, - redact.Safe(hitRate(m.Filter.Hits, m.Filter.Misses))) + + w.Printf("Table iters: %d\n", redact.Safe(m.TableIters)) + w.Printf("Filter utility: %.1f%%\n", redact.Safe(hitRate(m.Filter.Hits, m.Filter.Misses))) + w.Printf("Ingestions: %d as flushable: %d (%s in %d tables)\n", + redact.Safe(m.Ingest.Count), + redact.Safe(m.Flush.AsIngestCount), + humanize.Bytes.Uint64(m.Flush.AsIngestBytes), + redact.Safe(m.Flush.AsIngestTableCount)) } func hitRate(hits, misses int64) float64 { - sum := hits + misses - if sum == 0 { + return percent(hits, hits+misses) +} + +func percent(numerator, denominator int64) float64 { + if denominator == 0 { return 0 } - return 100 * float64(hits) / float64(sum) + return 100 * float64(numerator) / float64(denominator) +} + +// StringForTests is identical to m.String() on 64-bit platforms. It is used to +// provide a platform-independent result for tests. +func (m *Metrics) StringForTests() string { + mCopy := *m + if math.MaxInt == math.MaxInt32 { + // This is the difference in Sizeof(sstable.Reader{})) between 64 and 32 bit + // platforms. + const tableCacheSizeAdjustment = 212 + mCopy.TableCache.Size += mCopy.TableCache.Count * tableCacheSizeAdjustment + } + return redact.StringWithoutMarkers(&mCopy) } diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/noop_readahead.go b/vendor/github.com/cockroachdb/pebble/objstorage/noop_readahead.go index beb0d92..72c0a85 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/noop_readahead.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/noop_readahead.go @@ -4,26 +4,31 @@ package objstorage -import "io" +import "context" // NoopReadHandle can be used by Readable implementations that don't // support read-ahead. type NoopReadHandle struct { - io.ReaderAt + readable Readable } // MakeNoopReadHandle initializes a NoopReadHandle. -func MakeNoopReadHandle(r io.ReaderAt) NoopReadHandle { - return NoopReadHandle{ReaderAt: r} +func MakeNoopReadHandle(r Readable) NoopReadHandle { + return NoopReadHandle{readable: r} } var _ ReadHandle = (*NoopReadHandle)(nil) +// ReadAt is part of the ReadHandle interface. +func (h *NoopReadHandle) ReadAt(ctx context.Context, p []byte, off int64) error { + return h.readable.ReadAt(ctx, p, off) +} + // Close is part of the ReadHandle interface. func (*NoopReadHandle) Close() error { return nil } -// MaxReadahead is part of the ReadHandle interface. -func (*NoopReadHandle) MaxReadahead() {} +// SetupForCompaction is part of the ReadHandle interface. +func (*NoopReadHandle) SetupForCompaction() {} // RecordCacheHit is part of the ReadHandle interface. -func (*NoopReadHandle) RecordCacheHit(offset, size int64) {} +func (*NoopReadHandle) RecordCacheHit(_ context.Context, offset, size int64) {} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorage.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorage.go new file mode 100644 index 0000000..bbb5b6e --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorage.go @@ -0,0 +1,324 @@ +// Copyright 2023 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 objstorage + +import ( + "context" + "fmt" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/vfs" +) + +// Readable is the handle for an object that is open for reading. +type Readable interface { + // ReadAt reads len(p) bytes into p starting at offset off. + // + // Does not return partial results; if off + len(p) is past the end of the + // object, an error is returned. + // + // Clients of ReadAt can execute parallel ReadAt calls on the + // same Readable. + ReadAt(ctx context.Context, p []byte, off int64) error + + Close() error + + // Size returns the size of the object. + Size() int64 + + // NewReadHandle creates a read handle for ReadAt requests that are related + // and can benefit from optimizations like read-ahead. + // + // The ReadHandle must be closed before the Readable is closed. + // + // Multiple separate ReadHandles can be used. + NewReadHandle(ctx context.Context) ReadHandle +} + +// ReadHandle is used to perform reads that are related and might benefit from +// optimizations like read-ahead. +type ReadHandle interface { + // ReadAt reads len(p) bytes into p starting at offset off. + // + // Does not return partial results; if off + len(p) is past the end of the + // object, an error is returned. + // + // Parallel ReadAt calls on the same ReadHandle are not allowed. + ReadAt(ctx context.Context, p []byte, off int64) error + + Close() error + + // SetupForCompaction informs the implementation that the read handle will + // be used to read data blocks for a compaction. The implementation can expect + // sequential reads, and can decide to not retain data in any caches. + SetupForCompaction() + + // RecordCacheHit informs the implementation that we were able to retrieve a + // block from cache. This is useful for example when the implementation is + // trying to detect a sequential reading pattern. + RecordCacheHit(ctx context.Context, offset, size int64) +} + +// Writable is the handle for an object that is open for writing. +// Either Finish or Abort must be called. +type Writable interface { + // Write writes len(p) bytes from p to the underlying object. The data is not + // guaranteed to be durable until Finish is called. + // + // Note that Write *is* allowed to modify the slice passed in, whether + // temporarily or permanently. Callers of Write need to take this into + // account. + Write(p []byte) error + + // Finish completes the object and makes the data durable. + // No further calls are allowed after calling Finish. + Finish() error + + // Abort gives up on finishing the object. There is no guarantee about whether + // the object exists after calling Abort. + // No further calls are allowed after calling Abort. + Abort() +} + +// ObjectMetadata contains the metadata required to be able to access an object. +type ObjectMetadata struct { + DiskFileNum base.DiskFileNum + FileType base.FileType + + // The fields below are only set if the object is on remote storage. + Remote struct { + // CreatorID identifies the DB instance that originally created the object. + // + // Only used when CustomObjectName is not set. + CreatorID CreatorID + // CreatorFileNum is the identifier for the object within the context of the + // DB instance that originally created the object. + // + // Only used when CustomObjectName is not set. + CreatorFileNum base.DiskFileNum + // CustomObjectName (if it is set) overrides the object name that is normally + // derived from the CreatorID and CreatorFileNum. + CustomObjectName string + // CleanupMethod indicates the method for cleaning up unused shared objects. + CleanupMethod SharedCleanupMethod + // Locator identifies the remote.Storage implementation for this object. + Locator remote.Locator + // Storage is the remote.Storage object corresponding to the Locator. Used + // to avoid lookups in hot paths. + Storage remote.Storage + } +} + +// IsRemote returns true if the object is on remote storage. +func (meta *ObjectMetadata) IsRemote() bool { + return meta.IsShared() || meta.IsExternal() +} + +// IsExternal returns true if the object is on remote storage but is not owned +// by any Pebble instances in the cluster. +func (meta *ObjectMetadata) IsExternal() bool { + return meta.Remote.CustomObjectName != "" +} + +// IsShared returns true if the object is on remote storage and is owned by a +// Pebble instance in the cluster (potentially shared between multiple +// instances). +func (meta *ObjectMetadata) IsShared() bool { + return meta.Remote.CreatorID.IsSet() +} + +// AssertValid checks that the metadata is sane. +func (meta *ObjectMetadata) AssertValid() { + if !meta.IsRemote() { + // Verify all Remote fields are empty. + if meta.Remote != (ObjectMetadata{}).Remote { + panic(errors.AssertionFailedf("meta.Remote not empty: %#v", meta.Remote)) + } + } else { + if meta.Remote.CustomObjectName != "" { + if meta.Remote.CreatorID == 0 { + panic(errors.AssertionFailedf("CreatorID not set")) + } + if meta.Remote.CreatorFileNum == base.FileNum(0).DiskFileNum() { + panic(errors.AssertionFailedf("CreatorFileNum not set")) + } + } + if meta.Remote.CleanupMethod != SharedNoCleanup && meta.Remote.CleanupMethod != SharedRefTracking { + panic(errors.AssertionFailedf("invalid CleanupMethod %d", meta.Remote.CleanupMethod)) + } + if meta.Remote.Storage == nil { + panic(errors.AssertionFailedf("Storage not set")) + } + } +} + +// CreatorID identifies the DB instance that originally created a shared object. +// This ID is incorporated in backing object names. +// Must be non-zero. +type CreatorID uint64 + +// IsSet returns true if the CreatorID is not zero. +func (c CreatorID) IsSet() bool { return c != 0 } + +func (c CreatorID) String() string { return fmt.Sprintf("%d", c) } + +// SharedCleanupMethod indicates the method for cleaning up unused shared objects. +type SharedCleanupMethod uint8 + +const ( + // SharedRefTracking is used for shared objects for which objstorage providers + // keep track of references via reference marker objects. + SharedRefTracking SharedCleanupMethod = iota + + // SharedNoCleanup is used for remote objects that are managed externally; the + // objstorage provider never deletes such objects. + SharedNoCleanup +) + +// OpenOptions contains optional arguments for OpenForReading. +type OpenOptions struct { + // MustExist triggers a fatal error if the file does not exist. The fatal + // error message contains extra information helpful for debugging. + MustExist bool +} + +// CreateOptions contains optional arguments for Create. +type CreateOptions struct { + // PreferSharedStorage causes the object to be created on shared storage if + // the provider has shared storage configured. + PreferSharedStorage bool + + // SharedCleanupMethod is used for the object when it is created on shared storage. + // The default (zero) value is SharedRefTracking. + SharedCleanupMethod SharedCleanupMethod +} + +// Provider is a singleton object used to access and manage objects. +// +// An object is conceptually like a large immutable file. The main use of +// objects is for storing sstables; in the future it could also be used for blob +// storage. +// +// The Provider can only manage objects that it knows about - either objects +// created by the provider, or existing objects the Provider was informed about +// via AddObjects. +// +// Objects are currently backed by a vfs.File or a remote.Storage object. +type Provider interface { + // OpenForReading opens an existing object. + OpenForReading( + ctx context.Context, fileType base.FileType, FileNum base.DiskFileNum, opts OpenOptions, + ) (Readable, error) + + // Create creates a new object and opens it for writing. + // + // The object is not guaranteed to be durable (accessible in case of crashes) + // until Sync is called. + Create( + ctx context.Context, fileType base.FileType, FileNum base.DiskFileNum, opts CreateOptions, + ) (w Writable, meta ObjectMetadata, err error) + + // Remove removes an object. + // + // The object is not guaranteed to be durably removed until Sync is called. + Remove(fileType base.FileType, FileNum base.DiskFileNum) error + + // Sync flushes the metadata from creation or removal of objects since the last Sync. + // This includes objects that have been Created but for which + // Writable.Finish() has not yet been called. + Sync() error + + // LinkOrCopyFromLocal creates a new object that is either a copy of a given + // local file or a hard link (if the new object is created on the same FS, and + // if the FS supports it). + // + // The object is not guaranteed to be durable (accessible in case of crashes) + // until Sync is called. + LinkOrCopyFromLocal( + ctx context.Context, + srcFS vfs.FS, + srcFilePath string, + dstFileType base.FileType, + dstFileNum base.DiskFileNum, + opts CreateOptions, + ) (ObjectMetadata, error) + + // Lookup returns the metadata of an object that is already known to the Provider. + // Does not perform any I/O. + Lookup(fileType base.FileType, FileNum base.DiskFileNum) (ObjectMetadata, error) + + // Path returns an internal, implementation-dependent path for the object. It is + // meant to be used for informational purposes (like logging). + Path(meta ObjectMetadata) string + + // Size returns the size of the object. + Size(meta ObjectMetadata) (int64, error) + + // List returns the objects currently known to the provider. Does not perform any I/O. + List() []ObjectMetadata + + // SetCreatorID sets the CreatorID which is needed in order to use shared + // objects. Remote object usage is disabled until this method is called the + // first time. Once set, the Creator ID is persisted and cannot change. + // + // Cannot be called if shared storage is not configured for the provider. + SetCreatorID(creatorID CreatorID) error + + // IsSharedForeign returns whether this object is owned by a different node. + IsSharedForeign(meta ObjectMetadata) bool + + // RemoteObjectBacking encodes the remote object metadata for the given object. + RemoteObjectBacking(meta *ObjectMetadata) (RemoteObjectBackingHandle, error) + + // CreateExternalObjectBacking creates a backing for an existing object with a + // custom object name. The object is considered to be managed outside of + // Pebble and will never be removed by Pebble. + CreateExternalObjectBacking(locator remote.Locator, objName string) (RemoteObjectBacking, error) + + // AttachRemoteObjects registers existing remote objects with this provider. + AttachRemoteObjects(objs []RemoteObjectToAttach) ([]ObjectMetadata, error) + + Close() error + + // IsNotExistError indicates whether the error is known to report that a file or + // directory does not exist. + IsNotExistError(err error) bool + + // Metrics returns metrics about objstorage. Currently, it only returns metrics + // about the shared cache. + Metrics() sharedcache.Metrics +} + +// RemoteObjectBacking encodes the metadata necessary to incorporate a shared +// object into a different Pebble instance. The encoding is specific to a given +// Provider implementation. +type RemoteObjectBacking []byte + +// RemoteObjectBackingHandle is a container for a RemoteObjectBacking which +// ensures that the backing stays valid. A backing can otherwise become invalid +// if this provider unrefs the shared object. The RemoteObjectBackingHandle +// delays any unref until Close. +type RemoteObjectBackingHandle interface { + // Get returns the backing. The backing is only guaranteed to be valid until + // Close is called (or until the Provider is closed). If Close was already + // called, returns an error. + Get() (RemoteObjectBacking, error) + Close() +} + +// RemoteObjectToAttach contains the arguments needed to attach an existing remote object. +type RemoteObjectToAttach struct { + // FileNum is the file number that will be used to refer to this object (in + // the context of this instance). + FileNum base.DiskFileNum + FileType base.FileType + // Backing contains the metadata for the remote object backing (normally + // generated from a different instance, but using the same Provider + // implementation). + Backing RemoteObjectBacking +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing.go new file mode 100644 index 0000000..2672636 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing.go @@ -0,0 +1,68 @@ +// Copyright 2023 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 objiotracing + +import "github.com/cockroachdb/pebble/internal/base" + +// OpType indicates the type of operation. +type OpType uint8 + +// OpType values. +const ( + ReadOp OpType = iota + WriteOp + // RecordCacheHitOp happens when a read is satisfied from the block cache. See + // objstorage.ReadHandle.RecordCacheHit(). + RecordCacheHitOp + // SetupForCompactionOp is a "meta operation" that configures a read handle + // for large sequential reads. See objstorage.ReadHandle.SetupForCompaction(). + SetupForCompactionOp +) + +// Reason indicates the higher-level context of the operation. +type Reason uint8 + +// Reason values. +const ( + UnknownReason Reason = iota + ForFlush + ForCompaction + ForIngestion + // TODO(radu): add ForUserFacing. +) + +// BlockType indicates the type of data block relevant to an operation. +type BlockType uint8 + +// BlockType values. +const ( + UnknownBlock BlockType = iota + DataBlock + ValueBlock + FilterBlock + MetadataBlock +) + +// Event is the on-disk format of a tracing event. It is exported here so that +// trace processing tools can use it by importing this package. +type Event struct { + // Event start time as a Unix time (see time.Time.StartUnixNano()). + // Note that recorded events are not necessarily ordered by time - this is + // because separate event "streams" use local buffers (for performance). + StartUnixNano int64 + Op OpType + Reason Reason + BlockType BlockType + // LSM level plus one (with 0 indicating unknown level). + LevelPlusOne uint8 + // Hardcoded padding so that struct layout doesn't depend on architecture. + _ uint32 + FileNum base.FileNum + // HandleID is a unique identifier corresponding to an objstorage.ReadHandle; + // only set for read operations performed through a ReadHandle. + HandleID uint64 + Offset int64 + Size int64 +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_off.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_off.go new file mode 100644 index 0000000..a4923ab --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_off.go @@ -0,0 +1,59 @@ +// Copyright 2023 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. + +//go:build !pebble_obj_io_tracing +// +build !pebble_obj_io_tracing + +package objiotracing + +import ( + "context" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/vfs" +) + +// Enabled is used to short circuit tracing-related code in regular builds. +const Enabled = false + +// Tracer manages the writing of object IO traces to files. +type Tracer struct{} + +// Open creates a Tracer which generates trace files in the given directory. +// Each trace file contains a series of Events (as they are in memory). +func Open(fs vfs.FS, fsDir string) *Tracer { + return nil +} + +// Close the tracer, flushing any remaining events. +func (*Tracer) Close() {} + +// WrapReadable wraps an objstorage.Readable with one that generates tracing +// events. +func (*Tracer) WrapReadable( + ctx context.Context, r objstorage.Readable, fileNum base.DiskFileNum, +) objstorage.Readable { + return r +} + +// WrapWritable wraps an objstorage.Writable with one that generates tracing +// events. +func (t *Tracer) WrapWritable( + ctx context.Context, w objstorage.Writable, fileNum base.DiskFileNum, +) objstorage.Writable { + return w +} + +// WithReason creates a context that has an associated Reason (which ends up in +// traces created under that context). +func WithReason(ctx context.Context, reason Reason) context.Context { return ctx } + +// WithBlockType creates a context that has an associated BlockType (which ends up in +// traces created under that context). +func WithBlockType(ctx context.Context, blockType BlockType) context.Context { return ctx } + +// WithLevel creates a context that has an associated level (which ends up in +// traces created under that context). +func WithLevel(ctx context.Context, level int) context.Context { return ctx } diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_on.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_on.go new file mode 100644 index 0000000..0680b34 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing/obj_io_tracing_on.go @@ -0,0 +1,410 @@ +// Copyright 2023 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. + +//go:build pebble_obj_io_tracing +// +build pebble_obj_io_tracing + +package objiotracing + +import ( + "bufio" + "context" + "fmt" + "math/rand" + "sync" + "sync/atomic" + "time" + "unsafe" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/vfs" +) + +// Enabled is used to short circuit tracing-related code in regular builds. +const Enabled = true + +// Tracer manages the writing of object IO traces to files. +// +// The tracer runs a background worker goroutine which receives trace event +// buffers over a channel and dumps them to IOTRACES- files. Wrapper +// implementations for Readable, ReadHandle, Writable are producers of traces; +// they maintain internal buffers of events which get flushed to the buffered +// channel when they get full. This allows for minimal synchronization per IO +// (as for most of these structures, an instance only allows a single IO at a +// time). +type Tracer struct { + fs vfs.FS + fsDir string + + handleID atomic.Uint64 + + workerStopCh chan struct{} + workerDataCh chan eventBuf + workerWait sync.WaitGroup +} + +// Open creates a Tracer which generates trace files in the given directory. +// Each trace file contains a series of Events (as they are in memory). +func Open(fs vfs.FS, fsDir string) *Tracer { + t := &Tracer{ + fs: fs, + fsDir: fsDir, + workerStopCh: make(chan struct{}), + workerDataCh: make(chan eventBuf, channelBufSize), + } + + t.handleID.Store(uint64(rand.NewSource(time.Now().UnixNano()).Int63())) + + t.workerWait.Add(1) + go t.workerLoop() + return t +} + +// Close the tracer, flushing any remaining events. +func (t *Tracer) Close() { + if t.workerStopCh == nil { + return + } + // Tell the worker to stop and wait for it to finish up. + close(t.workerStopCh) + t.workerWait.Wait() + t.workerStopCh = nil +} + +// WrapWritable wraps an objstorage.Writable with one that generates tracing +// events. +func (t *Tracer) WrapWritable( + ctx context.Context, w objstorage.Writable, fileNum base.FileNum, +) objstorage.Writable { + return &writable{ + w: w, + fileNum: fileNum, + g: makeEventGenerator(ctx, t), + } +} + +type writable struct { + w objstorage.Writable + fileNum base.FileNum + curOffset int64 + g eventGenerator +} + +var _ objstorage.Writable = (*writable)(nil) + +// Write is part of the objstorage.Writable interface. +func (w *writable) Write(p []byte) error { + w.g.add(context.Background(), Event{ + Op: WriteOp, + FileNum: w.fileNum, + Offset: w.curOffset, + Size: int64(len(p)), + }) + // If w.w.Write(p) returns an error, a new writable + // will be used, so even tho all of p may not have + // been written to the underlying "file", it is okay + // to add len(p) to curOffset. + w.curOffset += int64(len(p)) + return w.w.Write(p) +} + +// Finish is part of the objstorage.Writable interface. +func (w *writable) Finish() error { + w.g.flush() + return w.w.Finish() +} + +// Abort is part of the objstorage.Writable interface. +func (w *writable) Abort() { + w.g.flush() + w.w.Abort() +} + +// WrapReadable wraps an objstorage.Readable with one that generates tracing +// events. +func (t *Tracer) WrapReadable( + ctx context.Context, r objstorage.Readable, fileNum base.FileNum, +) objstorage.Readable { + res := &readable{ + r: r, + fileNum: fileNum, + } + res.mu.g = makeEventGenerator(ctx, t) + return res +} + +type readable struct { + r objstorage.Readable + fileNum base.FileNum + mu struct { + sync.Mutex + g eventGenerator + } +} + +var _ objstorage.Readable = (*readable)(nil) + +// ReadAt is part of the objstorage.Readable interface. +func (r *readable) ReadAt(ctx context.Context, v []byte, off int64) (n int, err error) { + r.mu.Lock() + r.mu.g.add(ctx, Event{ + Op: ReadOp, + FileNum: r.fileNum, + Offset: off, + Size: int64(len(v)), + }) + r.mu.Unlock() + return r.r.ReadAt(ctx, v, off) +} + +// Close is part of the objstorage.Readable interface. +func (r *readable) Close() error { + r.mu.g.flush() + return r.r.Close() +} + +// Size is part of the objstorage.Readable interface. +func (r *readable) Size() int64 { + return r.r.Size() +} + +// NewReadHandle is part of the objstorage.Readable interface. +func (r *readable) NewReadHandle(ctx context.Context) objstorage.ReadHandle { + // It's safe to get the tracer from the generator without the mutex since it never changes. + t := r.mu.g.t + return &readHandle{ + rh: r.r.NewReadHandle(ctx), + fileNum: r.fileNum, + handleID: t.handleID.Add(1), + g: makeEventGenerator(ctx, t), + } +} + +type readHandle struct { + rh objstorage.ReadHandle + fileNum base.FileNum + handleID uint64 + g eventGenerator +} + +var _ objstorage.ReadHandle = (*readHandle)(nil) + +// ReadAt is part of the objstorage.ReadHandle interface. +func (rh *readHandle) ReadAt(ctx context.Context, p []byte, off int64) (n int, err error) { + rh.g.add(ctx, Event{ + Op: ReadOp, + FileNum: rh.fileNum, + HandleID: rh.handleID, + Offset: off, + Size: int64(len(p)), + }) + return rh.rh.ReadAt(ctx, p, off) +} + +// Close is part of the objstorage.ReadHandle interface. +func (rh *readHandle) Close() error { + rh.g.flush() + return rh.rh.Close() +} + +// SetupForCompaction is part of the objstorage.ReadHandle interface. +func (rh *readHandle) SetupForCompaction() { + rh.g.add(context.Background(), Event{ + Op: SetupForCompactionOp, + FileNum: rh.fileNum, + HandleID: rh.handleID, + }) + rh.rh.SetupForCompaction() +} + +// RecordCacheHit is part of the objstorage.ReadHandle interface. +func (rh *readHandle) RecordCacheHit(ctx context.Context, offset, size int64) { + rh.g.add(ctx, Event{ + Op: RecordCacheHitOp, + FileNum: rh.fileNum, + HandleID: rh.handleID, + Offset: offset, + Size: size, + }) + rh.rh.RecordCacheHit(ctx, offset, size) +} + +type ctxInfo struct { + reason Reason + blockType BlockType + levelPlusOne uint8 +} + +func mergeCtxInfo(base, other ctxInfo) ctxInfo { + res := other + if res.reason == 0 { + res.reason = base.reason + } + if res.blockType == 0 { + res.blockType = base.blockType + } + if res.levelPlusOne == 0 { + res.levelPlusOne = base.levelPlusOne + } + return res +} + +type ctxInfoKey struct{} + +func withInfo(ctx context.Context, info ctxInfo) context.Context { + return context.WithValue(ctx, ctxInfoKey{}, info) +} + +func infoFromCtx(ctx context.Context) ctxInfo { + res := ctx.Value(ctxInfoKey{}) + if res == nil { + return ctxInfo{} + } + return res.(ctxInfo) +} + +// WithReason creates a context that has an associated Reason (which ends up in +// traces created under that context). +func WithReason(ctx context.Context, reason Reason) context.Context { + info := infoFromCtx(ctx) + info.reason = reason + return withInfo(ctx, info) +} + +// WithBlockType creates a context that has an associated BlockType (which ends up in +// traces created under that context). +func WithBlockType(ctx context.Context, blockType BlockType) context.Context { + info := infoFromCtx(ctx) + info.blockType = blockType + return withInfo(ctx, info) +} + +// WithLevel creates a context that has an associated level (which ends up in +// traces created under that context). +func WithLevel(ctx context.Context, level int) context.Context { + info := infoFromCtx(ctx) + info.levelPlusOne = uint8(level) + 1 + return withInfo(ctx, info) +} + +const ( + eventSize = int(unsafe.Sizeof(Event{})) + targetEntriesPerFile = 256 * 1024 * 1024 / eventSize // 256MB files + eventsPerBuf = 16 + channelBufSize = 512 * 1024 / eventsPerBuf // 512K events. + bytesPerFileSync = 128 * 1024 +) + +type eventBuf struct { + events [eventsPerBuf]Event + num int +} + +type eventGenerator struct { + t *Tracer + baseCtxInfo ctxInfo + buf eventBuf +} + +func makeEventGenerator(ctx context.Context, t *Tracer) eventGenerator { + return eventGenerator{ + t: t, + baseCtxInfo: infoFromCtx(ctx), + } +} + +func (g *eventGenerator) flush() { + if g.buf.num > 0 { + g.t.workerDataCh <- g.buf + g.buf.num = 0 + } +} + +func (g *eventGenerator) add(ctx context.Context, e Event) { + e.StartUnixNano = time.Now().UnixNano() + info := infoFromCtx(ctx) + info = mergeCtxInfo(g.baseCtxInfo, info) + e.Reason = info.reason + e.BlockType = info.blockType + e.LevelPlusOne = info.levelPlusOne + if g.buf.num == eventsPerBuf { + g.flush() + } + g.buf.events[g.buf.num] = e + g.buf.num++ +} + +type workerState struct { + curFile vfs.File + curBW *bufio.Writer + numEntriesInFile int +} + +func (t *Tracer) workerLoop() { + defer t.workerWait.Done() + stopCh := t.workerStopCh + dataCh := t.workerDataCh + var state workerState + t.workerNewFile(&state) + for { + select { + case <-stopCh: + close(dataCh) + // Flush any remaining traces. + for data := range dataCh { + t.workerWriteTraces(&state, data) + } + t.workerCloseFile(&state) + return + + case data := <-dataCh: + t.workerWriteTraces(&state, data) + } + } +} + +func (t *Tracer) workerWriteTraces(state *workerState, data eventBuf) { + if state.numEntriesInFile >= targetEntriesPerFile { + t.workerCloseFile(state) + t.workerNewFile(state) + } + state.numEntriesInFile += data.num + p := unsafe.Pointer(&data.events[0]) + b := unsafe.Slice((*byte)(p), eventSize*data.num) + if _, err := state.curBW.Write(b); err != nil { + panic(err) + } +} + +func (t *Tracer) workerNewFile(state *workerState) { + filename := fmt.Sprintf("IOTRACES-%s", time.Now().UTC().Format(time.RFC3339Nano)) + + file, err := t.fs.Create(t.fs.PathJoin(t.fsDir, filename)) + if err != nil { + panic(err) + } + file = vfs.NewSyncingFile(file, vfs.SyncingFileOptions{ + BytesPerSync: bytesPerFileSync, + }) + state.curFile = file + state.curBW = bufio.NewWriter(file) + state.numEntriesInFile = 0 +} + +func (t *Tracer) workerCloseFile(state *workerState) { + if state.curFile != nil { + if err := state.curBW.Flush(); err != nil { + panic(err) + } + if err := state.curFile.Sync(); err != nil { + panic(err) + } + if err := state.curFile.Close(); err != nil { + panic(err) + } + state.curFile = nil + state.curBW = nil + } +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go new file mode 100644 index 0000000..d0f7450 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/provider.go @@ -0,0 +1,522 @@ +// Copyright 2023 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 objstorageprovider + +import ( + "context" + "io" + "os" + "sort" + "sync" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/errors/oserror" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/vfs" +) + +// provider is the implementation of objstorage.Provider. +type provider struct { + st Settings + + fsDir vfs.File + + tracer *objiotracing.Tracer + + remote remoteSubsystem + + mu struct { + sync.RWMutex + + remote struct { + // catalogBatch accumulates remote object creations and deletions until + // Sync is called. + catalogBatch remoteobjcat.Batch + + storageObjects map[remote.Locator]remote.Storage + } + + // localObjectsChanged is set if non-remote objects were created or deleted + // but Sync was not yet called. + localObjectsChanged bool + + // knownObjects maintains information about objects that are known to the provider. + // It is initialized with the list of files in the manifest when we open a DB. + knownObjects map[base.DiskFileNum]objstorage.ObjectMetadata + + // protectedObjects are objects that cannot be unreferenced because they + // have outstanding SharedObjectBackingHandles. The value is a count of outstanding handles + protectedObjects map[base.DiskFileNum]int + } +} + +var _ objstorage.Provider = (*provider)(nil) + +// Settings that must be specified when creating the provider. +type Settings struct { + Logger base.Logger + + // Local filesystem configuration. + FS vfs.FS + FSDirName string + + // FSDirInitialListing is a listing of FSDirName at the time of calling Open. + // + // This is an optional optimization to avoid double listing on Open when the + // higher layer already has a listing. When nil, we obtain the listing on + // Open. + FSDirInitialListing []string + + // Cleaner cleans obsolete files from the local filesystem. + // + // The default cleaner uses the DeleteCleaner. + FSCleaner base.Cleaner + + // NoSyncOnClose decides whether the implementation will enforce a + // close-time synchronization (e.g., fdatasync() or sync_file_range()) + // on files it writes to. Setting this to true removes the guarantee for a + // sync on close. Some implementations can still issue a non-blocking sync. + NoSyncOnClose bool + + // BytesPerSync enables periodic syncing of files in order to smooth out + // writes to disk. This option does not provide any persistence guarantee, but + // is used to avoid latency spikes if the OS automatically decides to write + // out a large chunk of dirty filesystem buffers. + BytesPerSync int + + // Fields here are set only if the provider is to support remote objects + // (experimental). + Remote struct { + StorageFactory remote.StorageFactory + + // If CreateOnShared is non-zero, sstables are created on remote storage using + // the CreateOnSharedLocator (when the PreferSharedStorage create option is + // true). + CreateOnShared remote.CreateOnSharedStrategy + CreateOnSharedLocator remote.Locator + + // CacheSizeBytes is the size of the on-disk block cache for objects + // on remote storage. If it is 0, no cache is used. + CacheSizeBytes int64 + + // CacheBlockSize is the block size of the cache; if 0, the default of 32KB is used. + CacheBlockSize int + + // ShardingBlockSize is the size of a shard block. The cache is split into contiguous + // ShardingBlockSize units. The units are distributed across multiple independent shards + // of the cache, via a hash(offset) modulo num shards operation. The cache replacement + // policies operate at the level of shard, not whole cache. This is done to reduce lock + // contention. + // + // If ShardingBlockSize is 0, the default of 1 MB is used. + ShardingBlockSize int64 + + // The number of independent shards the cache leverages. Each shard is the same size, + // and a hash of filenum & offset map a read to a certain shard. If set to 0, + // 2*runtime.GOMAXPROCS is used as the shard count. + CacheShardCount int + + // TODO(radu): allow the cache to live on another FS/location (e.g. to use + // instance-local SSD). + } +} + +// DefaultSettings initializes default settings (with no remote storage), +// suitable for tests and tools. +func DefaultSettings(fs vfs.FS, dirName string) Settings { + return Settings{ + Logger: base.DefaultLogger, + FS: fs, + FSDirName: dirName, + FSCleaner: base.DeleteCleaner{}, + NoSyncOnClose: false, + BytesPerSync: 512 * 1024, // 512KB + } +} + +// Open creates the provider. +func Open(settings Settings) (objstorage.Provider, error) { + // Note: we can't just `return open(settings)` because in an error case we + // would return (*provider)(nil) which is not objstorage.Provider(nil). + p, err := open(settings) + if err != nil { + return nil, err + } + return p, nil +} + +func open(settings Settings) (p *provider, _ error) { + fsDir, err := settings.FS.OpenDir(settings.FSDirName) + if err != nil { + return nil, err + } + + defer func() { + if p == nil { + fsDir.Close() + } + }() + + p = &provider{ + st: settings, + fsDir: fsDir, + } + p.mu.knownObjects = make(map[base.DiskFileNum]objstorage.ObjectMetadata) + p.mu.protectedObjects = make(map[base.DiskFileNum]int) + + if objiotracing.Enabled { + p.tracer = objiotracing.Open(settings.FS, settings.FSDirName) + } + + // Add local FS objects. + if err := p.vfsInit(); err != nil { + return nil, err + } + + // Initialize remote subsystem (if configured) and add remote objects. + if err := p.remoteInit(); err != nil { + return nil, err + } + + return p, nil +} + +// Close is part of the objstorage.Provider interface. +func (p *provider) Close() error { + err := p.sharedClose() + if p.fsDir != nil { + err = firstError(err, p.fsDir.Close()) + p.fsDir = nil + } + if objiotracing.Enabled { + if p.tracer != nil { + p.tracer.Close() + p.tracer = nil + } + } + return err +} + +// OpenForReading opens an existing object. +func (p *provider) OpenForReading( + ctx context.Context, + fileType base.FileType, + fileNum base.DiskFileNum, + opts objstorage.OpenOptions, +) (objstorage.Readable, error) { + meta, err := p.Lookup(fileType, fileNum) + if err != nil { + if opts.MustExist { + p.st.Logger.Fatalf("%v", err) + } + return nil, err + } + + var r objstorage.Readable + if !meta.IsRemote() { + r, err = p.vfsOpenForReading(ctx, fileType, fileNum, opts) + } else { + r, err = p.remoteOpenForReading(ctx, meta, opts) + if err != nil && p.isNotExistError(meta, err) { + // Wrap the error so that IsNotExistError functions properly. + err = errors.Mark(err, os.ErrNotExist) + } + } + if err != nil { + return nil, err + } + if objiotracing.Enabled { + r = p.tracer.WrapReadable(ctx, r, fileNum) + } + return r, nil +} + +// Create creates a new object and opens it for writing. +// +// The object is not guaranteed to be durable (accessible in case of crashes) +// until Sync is called. +func (p *provider) Create( + ctx context.Context, + fileType base.FileType, + fileNum base.DiskFileNum, + opts objstorage.CreateOptions, +) (w objstorage.Writable, meta objstorage.ObjectMetadata, err error) { + if opts.PreferSharedStorage && p.st.Remote.CreateOnShared != remote.CreateOnSharedNone { + w, meta, err = p.sharedCreate(ctx, fileType, fileNum, p.st.Remote.CreateOnSharedLocator, opts) + } else { + w, meta, err = p.vfsCreate(ctx, fileType, fileNum) + } + if err != nil { + err = errors.Wrapf(err, "creating object %s", errors.Safe(fileNum)) + return nil, objstorage.ObjectMetadata{}, err + } + p.addMetadata(meta) + if objiotracing.Enabled { + w = p.tracer.WrapWritable(ctx, w, fileNum) + } + return w, meta, nil +} + +// Remove removes an object. +// +// Note that if the object is remote, the object is only (conceptually) removed +// from this provider. If other providers have references on the remote object, +// it will not be removed. +// +// The object is not guaranteed to be durably removed until Sync is called. +func (p *provider) Remove(fileType base.FileType, fileNum base.DiskFileNum) error { + meta, err := p.Lookup(fileType, fileNum) + if err != nil { + return err + } + + if !meta.IsRemote() { + err = p.vfsRemove(fileType, fileNum) + } else { + // TODO(radu): implement remote object removal (i.e. deref). + err = p.sharedUnref(meta) + if err != nil && p.isNotExistError(meta, err) { + // Wrap the error so that IsNotExistError functions properly. + err = errors.Mark(err, os.ErrNotExist) + } + } + if err != nil && !p.IsNotExistError(err) { + // We want to be able to retry a Remove, so we keep the object in our list. + // TODO(radu): we should mark the object as "zombie" and not allow any other + // operations. + return errors.Wrapf(err, "removing object %s", errors.Safe(fileNum)) + } + + p.removeMetadata(fileNum) + return err +} + +func (p *provider) isNotExistError(meta objstorage.ObjectMetadata, err error) bool { + if meta.Remote.Storage != nil { + return meta.Remote.Storage.IsNotExistError(err) + } + return oserror.IsNotExist(err) +} + +// IsNotExistError is part of the objstorage.Provider interface. +func (p *provider) IsNotExistError(err error) bool { + // We use errors.Mark(err, os.ErrNotExist) for not-exist errors coming from + // remote.Storage. + return oserror.IsNotExist(err) +} + +// Sync flushes the metadata from creation or removal of objects since the last Sync. +func (p *provider) Sync() error { + if err := p.vfsSync(); err != nil { + return err + } + if err := p.sharedSync(); err != nil { + return err + } + return nil +} + +// LinkOrCopyFromLocal creates a new object that is either a copy of a given +// local file or a hard link (if the new object is created on the same FS, and +// if the FS supports it). +// +// The object is not guaranteed to be durable (accessible in case of crashes) +// until Sync is called. +func (p *provider) LinkOrCopyFromLocal( + ctx context.Context, + srcFS vfs.FS, + srcFilePath string, + dstFileType base.FileType, + dstFileNum base.DiskFileNum, + opts objstorage.CreateOptions, +) (objstorage.ObjectMetadata, error) { + shared := opts.PreferSharedStorage && p.st.Remote.CreateOnShared != remote.CreateOnSharedNone + if !shared && srcFS == p.st.FS { + // Wrap the normal filesystem with one which wraps newly created files with + // vfs.NewSyncingFile. + fs := vfs.NewSyncingFS(p.st.FS, vfs.SyncingFileOptions{ + NoSyncOnClose: p.st.NoSyncOnClose, + BytesPerSync: p.st.BytesPerSync, + }) + dstPath := p.vfsPath(dstFileType, dstFileNum) + if err := vfs.LinkOrCopy(fs, srcFilePath, dstPath); err != nil { + return objstorage.ObjectMetadata{}, err + } + + meta := objstorage.ObjectMetadata{ + DiskFileNum: dstFileNum, + FileType: dstFileType, + } + p.addMetadata(meta) + return meta, nil + } + // Create the object and copy the data. + w, meta, err := p.Create(ctx, dstFileType, dstFileNum, opts) + if err != nil { + return objstorage.ObjectMetadata{}, err + } + f, err := srcFS.Open(srcFilePath, vfs.SequentialReadsOption) + if err != nil { + return objstorage.ObjectMetadata{}, err + } + defer f.Close() + buf := make([]byte, 64*1024) + for { + n, readErr := f.Read(buf) + if readErr != nil && readErr != io.EOF { + w.Abort() + return objstorage.ObjectMetadata{}, readErr + } + + if n > 0 { + if err := w.Write(buf[:n]); err != nil { + w.Abort() + return objstorage.ObjectMetadata{}, err + } + } + + if readErr == io.EOF { + break + } + } + if err := w.Finish(); err != nil { + return objstorage.ObjectMetadata{}, err + } + return meta, nil +} + +// Lookup is part of the objstorage.Provider interface. +func (p *provider) Lookup( + fileType base.FileType, fileNum base.DiskFileNum, +) (objstorage.ObjectMetadata, error) { + p.mu.RLock() + defer p.mu.RUnlock() + meta, ok := p.mu.knownObjects[fileNum] + if !ok { + return objstorage.ObjectMetadata{}, errors.Wrapf( + os.ErrNotExist, + "file %s (type %d) unknown to the objstorage provider", + errors.Safe(fileNum), errors.Safe(fileType), + ) + } + if meta.FileType != fileType { + return objstorage.ObjectMetadata{}, errors.AssertionFailedf( + "file %s type mismatch (known type %d, expected type %d)", + errors.Safe(fileNum), errors.Safe(meta.FileType), errors.Safe(fileType), + ) + } + return meta, nil +} + +// Path is part of the objstorage.Provider interface. +func (p *provider) Path(meta objstorage.ObjectMetadata) string { + if !meta.IsRemote() { + return p.vfsPath(meta.FileType, meta.DiskFileNum) + } + return p.remotePath(meta) +} + +// Size returns the size of the object. +func (p *provider) Size(meta objstorage.ObjectMetadata) (int64, error) { + if !meta.IsRemote() { + return p.vfsSize(meta.FileType, meta.DiskFileNum) + } + return p.remoteSize(meta) +} + +// List is part of the objstorage.Provider interface. +func (p *provider) List() []objstorage.ObjectMetadata { + p.mu.RLock() + defer p.mu.RUnlock() + res := make([]objstorage.ObjectMetadata, 0, len(p.mu.knownObjects)) + for _, meta := range p.mu.knownObjects { + res = append(res, meta) + } + sort.Slice(res, func(i, j int) bool { + return res[i].DiskFileNum.FileNum() < res[j].DiskFileNum.FileNum() + }) + return res +} + +// Metrics is part of the objstorage.Provider interface. +func (p *provider) Metrics() sharedcache.Metrics { + if p.remote.cache != nil { + return p.remote.cache.Metrics() + } + return sharedcache.Metrics{} +} + +func (p *provider) addMetadata(meta objstorage.ObjectMetadata) { + if invariants.Enabled { + meta.AssertValid() + } + p.mu.Lock() + defer p.mu.Unlock() + p.mu.knownObjects[meta.DiskFileNum] = meta + if meta.IsRemote() { + p.mu.remote.catalogBatch.AddObject(remoteobjcat.RemoteObjectMetadata{ + FileNum: meta.DiskFileNum, + FileType: meta.FileType, + CreatorID: meta.Remote.CreatorID, + CreatorFileNum: meta.Remote.CreatorFileNum, + Locator: meta.Remote.Locator, + CleanupMethod: meta.Remote.CleanupMethod, + }) + } else { + p.mu.localObjectsChanged = true + } +} + +func (p *provider) removeMetadata(fileNum base.DiskFileNum) { + p.mu.Lock() + defer p.mu.Unlock() + + meta, ok := p.mu.knownObjects[fileNum] + if !ok { + return + } + delete(p.mu.knownObjects, fileNum) + if meta.IsRemote() { + p.mu.remote.catalogBatch.DeleteObject(fileNum) + } else { + p.mu.localObjectsChanged = true + } +} + +// protectObject prevents the unreferencing of a remote object until +// unprotectObject is called. +func (p *provider) protectObject(fileNum base.DiskFileNum) { + p.mu.Lock() + defer p.mu.Unlock() + p.mu.protectedObjects[fileNum] = p.mu.protectedObjects[fileNum] + 1 +} + +func (p *provider) unprotectObject(fileNum base.DiskFileNum) { + p.mu.Lock() + defer p.mu.Unlock() + v := p.mu.protectedObjects[fileNum] + if invariants.Enabled && v == 0 { + panic("invalid protection count") + } + if v > 1 { + p.mu.protectedObjects[fileNum] = v - 1 + } else { + delete(p.mu.protectedObjects, fileNum) + // TODO(radu): check if the object is still in knownObject; if not, unref it + // now. + } +} + +func (p *provider) isProtected(fileNum base.DiskFileNum) bool { + p.mu.Lock() + defer p.mu.Unlock() + return p.mu.protectedObjects[fileNum] > 0 +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/readahead.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/readahead.go similarity index 80% rename from vendor/github.com/cockroachdb/pebble/objstorage/readahead.go rename to vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/readahead.go index 38984c5..e07017c 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/readahead.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/readahead.go @@ -2,7 +2,7 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -package objstorage +package objstorageprovider const ( // Constants for dynamic readahead of data blocks. Note that the size values @@ -11,15 +11,15 @@ const ( minFileReadsForReadahead = 2 // TODO(bilal): Have the initial size value be a factor of the block size, // as opposed to a hardcoded value. - initialReadaheadSize = 64 << 10 /* 64KB */ - maxReadaheadSize = 256 << 10 /* 256KB */ + initialReadaheadSize = 64 << 10 /* 64KB */ ) // readaheadState contains state variables related to readahead. Updated on // file reads. type readaheadState struct { // Number of sequential reads. - numReads int64 + numReads int64 + maxReadaheadSize int64 // Size issued to the next call to Prefetch. Starts at or above // initialReadaheadSize and grows exponentially until maxReadaheadSize. size int64 @@ -32,22 +32,23 @@ type readaheadState struct { limit int64 } -func makeReadaheadState() readaheadState { +func makeReadaheadState(maxReadaheadSize int64) readaheadState { return readaheadState{ - size: initialReadaheadSize, + size: initialReadaheadSize, + maxReadaheadSize: maxReadaheadSize, } } func (rs *readaheadState) recordCacheHit(offset, blockLength int64) { currentReadEnd := offset + blockLength if rs.numReads >= minFileReadsForReadahead { - if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { + if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize { // This is a read that would have resulted in a readahead, had it // not been a cache hit. rs.limit = currentReadEnd return } - if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+maxReadaheadSize { + if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+rs.maxReadaheadSize { // We read too far away from rs.limit to benefit from readahead in // any scenario. Reset all variables. rs.numReads = 1 @@ -61,7 +62,7 @@ func (rs *readaheadState) recordCacheHit(offset, blockLength int64) { // readahead. return } - if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { + if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize { // Blocks are being read sequentially and would benefit from readahead // down the line. rs.numReads++ @@ -89,28 +90,28 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { // as well as the interval where a read would benefit from read ahead: // [rs.limit, rs.limit + rs.size] // We increase the latter interval to - // [rs.limit, rs.limit + maxReadaheadSize] to account for cases where + // [rs.limit, rs.limit + rs.maxReadaheadSize] to account for cases where // readahead may not be beneficial with a small readahead size, but over // time the readahead size would increase exponentially to make it // beneficial. - if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { + if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize { // We are doing a read in the interval ahead of // the last readahead range. In the diagrams below, ++++ is the last // readahead range, ==== is the range represented by - // [rs.limit, rs.limit + maxReadaheadSize], and ---- is the range + // [rs.limit, rs.limit + rs.maxReadaheadSize], and ---- is the range // being read. // - // rs.limit rs.limit + maxReadaheadSize + // rs.limit rs.limit + rs.maxReadaheadSize // ++++++++++|===========================| // // |-------------| // offset currentReadEnd // // This case is also possible, as are all cases with an overlap - // between [rs.limit, rs.limit + maxReadaheadSize] and [offset, + // between [rs.limit, rs.limit + rs.maxReadaheadSize] and [offset, // currentReadEnd]: // - // rs.limit rs.limit + maxReadaheadSize + // rs.limit rs.limit + rs.maxReadaheadSize // ++++++++++|===========================| // // |-------------| @@ -122,19 +123,19 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { rs.prevSize = rs.size // Increase rs.size for the next read. rs.size *= 2 - if rs.size > maxReadaheadSize { - rs.size = maxReadaheadSize + if rs.size > rs.maxReadaheadSize { + rs.size = rs.maxReadaheadSize } return rs.prevSize } - if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+maxReadaheadSize { + if currentReadEnd < rs.limit-rs.prevSize || offset > rs.limit+rs.maxReadaheadSize { // The above conditional has rs.limit > rs.prevSize to confirm that // rs.limit - rs.prevSize would not underflow. // We read too far away from rs.limit to benefit from readahead in // any scenario. Reset all variables. // The case where we read too far ahead: // - // (rs.limit - rs.prevSize) (rs.limit) (rs.limit + maxReadaheadSize) + // (rs.limit - rs.prevSize) (rs.limit) (rs.limit + rs.maxReadaheadSize) // |+++++++++++++|=============| // // |-------------| @@ -142,7 +143,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { // // Or too far behind: // - // (rs.limit - rs.prevSize) (rs.limit) (rs.limit + maxReadaheadSize) + // (rs.limit - rs.prevSize) (rs.limit) (rs.limit + rs.maxReadaheadSize) // |+++++++++++++|=============| // // |-------------| @@ -160,7 +161,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { // readahead, but there's no reason to issue a readahead call at the // moment. // - // (rs.limit - rs.prevSize) (rs.limit + maxReadaheadSize) + // (rs.limit - rs.prevSize) (rs.limit + rs.maxReadaheadSize) // |+++++++++++++|===============| // (rs.limit) // @@ -170,11 +171,11 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { rs.numReads++ return 0 } - if currentReadEnd >= rs.limit && offset <= rs.limit+maxReadaheadSize { + if currentReadEnd >= rs.limit && offset <= rs.limit+rs.maxReadaheadSize { // Blocks are being read sequentially and would benefit from readahead // down the line. // - // (rs.limit) (rs.limit + maxReadaheadSize) + // (rs.limit) (rs.limit + rs.maxReadaheadSize) // |=============| // // |-------| @@ -186,7 +187,7 @@ func (rs *readaheadState) maybeReadahead(offset, blockLength int64) int64 { // We read too far ahead of the last read, or before it. This indicates // a random read, where readahead is not desirable. Reset all variables. // - // (rs.limit - maxReadaheadSize) (rs.limit) (rs.limit + maxReadaheadSize) + // (rs.limit - rs.maxReadaheadSize) (rs.limit) (rs.limit + rs.maxReadaheadSize) // |+++++++++++++|=============| // // |-------| diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote.go new file mode 100644 index 0000000..e8ee42b --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote.go @@ -0,0 +1,376 @@ +// Copyright 2023 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 objstorageprovider + +import ( + "context" + "fmt" + "runtime" + "sync" + "sync/atomic" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache" + "github.com/cockroachdb/pebble/objstorage/remote" +) + +// remoteSubsystem contains the provider fields related to remote storage. +// All fields remain unset if remote storage is not configured. +type remoteSubsystem struct { + catalog *remoteobjcat.Catalog + // catalogSyncMutex is used to correctly serialize two sharedSync operations. + // It must be acquired before the provider mutex. + catalogSyncMutex sync.Mutex + + cache *sharedcache.Cache + + // shared contains the fields relevant to shared objects, i.e. objects that + // are created by Pebble and potentially shared between Pebble instances. + shared struct { + // initialized guards access to the creatorID field. + initialized atomic.Bool + creatorID objstorage.CreatorID + initOnce sync.Once + + // checkRefsOnOpen controls whether we check the ref marker file when opening + // an object. Normally this is true when invariants are enabled (but the provider + // test tweaks this field). + checkRefsOnOpen bool + } +} + +// remoteInit initializes the remote object subsystem (if configured) and finds +// any remote objects. +func (p *provider) remoteInit() error { + if p.st.Remote.StorageFactory == nil { + return nil + } + catalog, contents, err := remoteobjcat.Open(p.st.FS, p.st.FSDirName) + if err != nil { + return errors.Wrapf(err, "pebble: could not open remote object catalog") + } + p.remote.catalog = catalog + p.remote.shared.checkRefsOnOpen = invariants.Enabled + + // The creator ID may or may not be initialized yet. + if contents.CreatorID.IsSet() { + p.remote.initShared(contents.CreatorID) + p.st.Logger.Infof("remote storage configured; creatorID = %s", contents.CreatorID) + } else { + p.st.Logger.Infof("remote storage configured; no creatorID yet") + } + + if p.st.Remote.CacheSizeBytes > 0 { + const defaultBlockSize = 32 * 1024 + blockSize := p.st.Remote.CacheBlockSize + if blockSize == 0 { + blockSize = defaultBlockSize + } + + const defaultShardingBlockSize = 1024 * 1024 + shardingBlockSize := p.st.Remote.ShardingBlockSize + if shardingBlockSize == 0 { + shardingBlockSize = defaultShardingBlockSize + } + + numShards := p.st.Remote.CacheShardCount + if numShards == 0 { + numShards = 2 * runtime.GOMAXPROCS(0) + } + + p.remote.cache, err = sharedcache.Open( + p.st.FS, p.st.Logger, p.st.FSDirName, blockSize, shardingBlockSize, p.st.Remote.CacheSizeBytes, numShards) + if err != nil { + return errors.Wrapf(err, "pebble: could not open remote object cache") + } + } + + for _, meta := range contents.Objects { + o := objstorage.ObjectMetadata{ + DiskFileNum: meta.FileNum, + FileType: meta.FileType, + } + o.Remote.CreatorID = meta.CreatorID + o.Remote.CreatorFileNum = meta.CreatorFileNum + o.Remote.CleanupMethod = meta.CleanupMethod + o.Remote.Locator = meta.Locator + o.Remote.CustomObjectName = meta.CustomObjectName + o.Remote.Storage, err = p.ensureStorageLocked(o.Remote.Locator) + if err != nil { + return errors.Wrapf(err, "creating remote.Storage object for locator '%s'", o.Remote.Locator) + } + if invariants.Enabled { + o.AssertValid() + } + p.mu.knownObjects[o.DiskFileNum] = o + } + return nil +} + +// initShared initializes the creator ID, allowing use of shared objects. +func (ss *remoteSubsystem) initShared(creatorID objstorage.CreatorID) { + ss.shared.initOnce.Do(func() { + ss.shared.creatorID = creatorID + ss.shared.initialized.Store(true) + }) +} + +func (p *provider) sharedClose() error { + if p.st.Remote.StorageFactory == nil { + return nil + } + var err error + if p.remote.cache != nil { + err = p.remote.cache.Close() + p.remote.cache = nil + } + if p.remote.catalog != nil { + err = firstError(err, p.remote.catalog.Close()) + p.remote.catalog = nil + } + return err +} + +// SetCreatorID is part of the objstorage.Provider interface. +func (p *provider) SetCreatorID(creatorID objstorage.CreatorID) error { + if p.st.Remote.StorageFactory == nil { + return errors.AssertionFailedf("attempt to set CreatorID but remote storage not enabled") + } + // Note: this call is a cheap no-op if the creator ID was already set. This + // call also checks if we are trying to change the ID. + if err := p.remote.catalog.SetCreatorID(creatorID); err != nil { + return err + } + if !p.remote.shared.initialized.Load() { + p.st.Logger.Infof("remote storage creatorID set to %s", creatorID) + p.remote.initShared(creatorID) + } + return nil +} + +// IsSharedForeign is part of the objstorage.Provider interface. +func (p *provider) IsSharedForeign(meta objstorage.ObjectMetadata) bool { + if !p.remote.shared.initialized.Load() { + return false + } + return meta.IsShared() && (meta.Remote.CreatorID != p.remote.shared.creatorID) +} + +func (p *provider) remoteCheckInitialized() error { + if p.st.Remote.StorageFactory == nil { + return errors.Errorf("remote object support not configured") + } + return nil +} + +func (p *provider) sharedCheckInitialized() error { + if err := p.remoteCheckInitialized(); err != nil { + return err + } + if !p.remote.shared.initialized.Load() { + return errors.Errorf("remote object support not available: remote creator ID not yet set") + } + return nil +} + +func (p *provider) sharedSync() error { + // Serialize parallel sync operations. Note that ApplyBatch is already + // serialized internally, but we want to make sure they get called with + // batches in the right order. + p.remote.catalogSyncMutex.Lock() + defer p.remote.catalogSyncMutex.Unlock() + + batch := func() remoteobjcat.Batch { + p.mu.Lock() + defer p.mu.Unlock() + res := p.mu.remote.catalogBatch.Copy() + p.mu.remote.catalogBatch.Reset() + return res + }() + + if batch.IsEmpty() { + return nil + } + + if err := p.remote.catalog.ApplyBatch(batch); err != nil { + // Put back the batch (for the next Sync), appending any operations that + // happened in the meantime. + p.mu.Lock() + defer p.mu.Unlock() + batch.Append(p.mu.remote.catalogBatch) + p.mu.remote.catalogBatch = batch + return err + } + + return nil +} + +func (p *provider) remotePath(meta objstorage.ObjectMetadata) string { + if meta.Remote.Locator != "" { + return fmt.Sprintf("remote-%s://%s", meta.Remote.Locator, remoteObjectName(meta)) + } + return "remote://" + remoteObjectName(meta) +} + +// sharedCreateRef creates a reference marker object. +func (p *provider) sharedCreateRef(meta objstorage.ObjectMetadata) error { + if err := p.sharedCheckInitialized(); err != nil { + return err + } + if meta.Remote.CleanupMethod != objstorage.SharedRefTracking { + return nil + } + refName := p.sharedObjectRefName(meta) + writer, err := meta.Remote.Storage.CreateObject(refName) + if err == nil { + // The object is empty, just close the writer. + err = writer.Close() + } + if err != nil { + return errors.Wrapf(err, "creating marker object %q", refName) + } + return nil +} + +func (p *provider) sharedCreate( + _ context.Context, + fileType base.FileType, + fileNum base.DiskFileNum, + locator remote.Locator, + opts objstorage.CreateOptions, +) (objstorage.Writable, objstorage.ObjectMetadata, error) { + if err := p.sharedCheckInitialized(); err != nil { + return nil, objstorage.ObjectMetadata{}, err + } + storage, err := p.ensureStorage(locator) + if err != nil { + return nil, objstorage.ObjectMetadata{}, err + } + meta := objstorage.ObjectMetadata{ + DiskFileNum: fileNum, + FileType: fileType, + } + meta.Remote.CreatorID = p.remote.shared.creatorID + meta.Remote.CreatorFileNum = fileNum + meta.Remote.CleanupMethod = opts.SharedCleanupMethod + meta.Remote.Locator = locator + meta.Remote.Storage = storage + + objName := remoteObjectName(meta) + writer, err := storage.CreateObject(objName) + if err != nil { + return nil, objstorage.ObjectMetadata{}, errors.Wrapf(err, "creating object %q", objName) + } + return &sharedWritable{ + p: p, + meta: meta, + storageWriter: writer, + }, meta, nil +} + +func (p *provider) remoteOpenForReading( + ctx context.Context, meta objstorage.ObjectMetadata, opts objstorage.OpenOptions, +) (objstorage.Readable, error) { + if err := p.remoteCheckInitialized(); err != nil { + return nil, err + } + // Verify we have a reference on this object; for performance reasons, we only + // do this in testing scenarios. + if p.remote.shared.checkRefsOnOpen && meta.Remote.CleanupMethod == objstorage.SharedRefTracking { + if err := p.sharedCheckInitialized(); err != nil { + return nil, err + } + refName := p.sharedObjectRefName(meta) + if _, err := meta.Remote.Storage.Size(refName); err != nil { + if meta.Remote.Storage.IsNotExistError(err) { + if opts.MustExist { + p.st.Logger.Fatalf("marker object %q does not exist", refName) + // TODO(radu): maybe list references for the object. + } + return nil, errors.Errorf("marker object %q does not exist", refName) + } + return nil, errors.Wrapf(err, "checking marker object %q", refName) + } + } + objName := remoteObjectName(meta) + reader, size, err := meta.Remote.Storage.ReadObject(ctx, objName) + if err != nil { + if opts.MustExist && meta.Remote.Storage.IsNotExistError(err) { + p.st.Logger.Fatalf("object %q does not exist", objName) + // TODO(radu): maybe list references for the object. + } + return nil, err + } + return p.newRemoteReadable(reader, size, meta.DiskFileNum), nil +} + +func (p *provider) remoteSize(meta objstorage.ObjectMetadata) (int64, error) { + if err := p.remoteCheckInitialized(); err != nil { + return 0, err + } + objName := remoteObjectName(meta) + return meta.Remote.Storage.Size(objName) +} + +// sharedUnref implements object "removal" with the remote backend. The ref +// marker object is removed and the backing object is removed only if there are +// no other ref markers. +func (p *provider) sharedUnref(meta objstorage.ObjectMetadata) error { + if meta.Remote.CleanupMethod == objstorage.SharedNoCleanup { + // Never delete objects in this mode. + return nil + } + if p.isProtected(meta.DiskFileNum) { + // TODO(radu): we need a mechanism to unref the object when it becomes + // unprotected. + return nil + } + + refName := p.sharedObjectRefName(meta) + // Tolerate a not-exists error. + if err := meta.Remote.Storage.Delete(refName); err != nil && !meta.Remote.Storage.IsNotExistError(err) { + return err + } + otherRefs, err := meta.Remote.Storage.List(sharedObjectRefPrefix(meta), "" /* delimiter */) + if err != nil { + return err + } + if len(otherRefs) == 0 { + objName := remoteObjectName(meta) + if err := meta.Remote.Storage.Delete(objName); err != nil && !meta.Remote.Storage.IsNotExistError(err) { + return err + } + } + return nil +} + +// ensureStorageLocked populates the remote.Storage object for the given +// locator, if necessary. p.mu must be held. +func (p *provider) ensureStorageLocked(locator remote.Locator) (remote.Storage, error) { + if p.mu.remote.storageObjects == nil { + p.mu.remote.storageObjects = make(map[remote.Locator]remote.Storage) + } + if res, ok := p.mu.remote.storageObjects[locator]; ok { + return res, nil + } + res, err := p.st.Remote.StorageFactory.CreateStorage(locator) + if err != nil { + return nil, err + } + + p.mu.remote.storageObjects[locator] = res + return res, nil +} + +// ensureStorage populates the remote.Storage object for the given locator, if necessary. +func (p *provider) ensureStorage(locator remote.Locator) (remote.Storage, error) { + p.mu.Lock() + defer p.mu.Unlock() + return p.ensureStorageLocked(locator) +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_backing.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_backing.go new file mode 100644 index 0000000..d302151 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_backing.go @@ -0,0 +1,303 @@ +// Copyright 2023 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 objstorageprovider + +import ( + "bytes" + "encoding/binary" + "io" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat" + "github.com/cockroachdb/pebble/objstorage/remote" +) + +const ( + tagCreatorID = 1 + tagCreatorFileNum = 2 + tagCleanupMethod = 3 + // tagRefCheckID encodes the information for a ref marker that needs to be + // checked when attaching this object to another provider. This is set to the + // creator ID and FileNum for the provider that encodes the backing, and + // allows the "target" provider to check that the "source" provider kept its + // reference on the object alive. + tagRefCheckID = 4 + // tagLocator encodes the remote.Locator; if absent the locator is "". It is + // followed by the locator string length and the locator string. + tagLocator = 5 + // tagLocator encodes a custom object name (if present). It is followed by the + // custom name string length and the string. + tagCustomObjectName = 6 + + // Any new tags that don't have the tagNotSafeToIgnoreMask bit set must be + // followed by the length of the data (so they can be skipped). + + // Any new tags that have the tagNotSafeToIgnoreMask bit set cause errors if + // they are encountered by earlier code that doesn't know the tag. + tagNotSafeToIgnoreMask = 64 +) + +func (p *provider) encodeRemoteObjectBacking( + meta *objstorage.ObjectMetadata, +) (objstorage.RemoteObjectBacking, error) { + if !meta.IsRemote() { + return nil, errors.AssertionFailedf("object %s not on remote storage", meta.DiskFileNum) + } + + buf := make([]byte, 0, binary.MaxVarintLen64*4) + buf = binary.AppendUvarint(buf, tagCreatorID) + buf = binary.AppendUvarint(buf, uint64(meta.Remote.CreatorID)) + // TODO(radu): encode file type as well? + buf = binary.AppendUvarint(buf, tagCreatorFileNum) + buf = binary.AppendUvarint(buf, uint64(meta.Remote.CreatorFileNum.FileNum())) + buf = binary.AppendUvarint(buf, tagCleanupMethod) + buf = binary.AppendUvarint(buf, uint64(meta.Remote.CleanupMethod)) + if meta.Remote.CleanupMethod == objstorage.SharedRefTracking { + buf = binary.AppendUvarint(buf, tagRefCheckID) + buf = binary.AppendUvarint(buf, uint64(p.remote.shared.creatorID)) + buf = binary.AppendUvarint(buf, uint64(meta.DiskFileNum.FileNum())) + } + if meta.Remote.Locator != "" { + buf = binary.AppendUvarint(buf, tagLocator) + buf = encodeString(buf, string(meta.Remote.Locator)) + } + if meta.Remote.CustomObjectName != "" { + buf = binary.AppendUvarint(buf, tagCustomObjectName) + buf = encodeString(buf, meta.Remote.CustomObjectName) + } + return buf, nil +} + +type remoteObjectBackingHandle struct { + backing objstorage.RemoteObjectBacking + fileNum base.DiskFileNum + p *provider +} + +func (s *remoteObjectBackingHandle) Get() (objstorage.RemoteObjectBacking, error) { + if s.backing == nil { + return nil, errors.Errorf("RemoteObjectBackingHandle.Get() called after Close()") + } + return s.backing, nil +} + +func (s *remoteObjectBackingHandle) Close() { + if s.backing != nil { + s.backing = nil + s.p.unprotectObject(s.fileNum) + } +} + +var _ objstorage.RemoteObjectBackingHandle = (*remoteObjectBackingHandle)(nil) + +// RemoteObjectBacking is part of the objstorage.Provider interface. +func (p *provider) RemoteObjectBacking( + meta *objstorage.ObjectMetadata, +) (objstorage.RemoteObjectBackingHandle, error) { + backing, err := p.encodeRemoteObjectBacking(meta) + if err != nil { + return nil, err + } + p.protectObject(meta.DiskFileNum) + return &remoteObjectBackingHandle{ + backing: backing, + fileNum: meta.DiskFileNum, + p: p, + }, nil +} + +// CreateExternalObjectBacking is part of the objstorage.Provider interface. +func (p *provider) CreateExternalObjectBacking( + locator remote.Locator, objName string, +) (objstorage.RemoteObjectBacking, error) { + var meta objstorage.ObjectMetadata + meta.Remote.Locator = locator + meta.Remote.CustomObjectName = objName + meta.Remote.CleanupMethod = objstorage.SharedNoCleanup + return p.encodeRemoteObjectBacking(&meta) +} + +type decodedBacking struct { + meta objstorage.ObjectMetadata + // refToCheck is set only when meta.Remote.CleanupMethod is RefTracking + refToCheck struct { + creatorID objstorage.CreatorID + fileNum base.DiskFileNum + } +} + +// decodeRemoteObjectBacking decodes the remote object metadata. +// +// Note that the meta.Remote.Storage field is not set. +func decodeRemoteObjectBacking( + fileType base.FileType, fileNum base.DiskFileNum, buf objstorage.RemoteObjectBacking, +) (decodedBacking, error) { + var creatorID, creatorFileNum, cleanupMethod, refCheckCreatorID, refCheckFileNum uint64 + var locator, customObjName string + br := bytes.NewReader(buf) + for { + tag, err := binary.ReadUvarint(br) + if err == io.EOF { + break + } + if err != nil { + return decodedBacking{}, err + } + switch tag { + case tagCreatorID: + creatorID, err = binary.ReadUvarint(br) + + case tagCreatorFileNum: + creatorFileNum, err = binary.ReadUvarint(br) + + case tagCleanupMethod: + cleanupMethod, err = binary.ReadUvarint(br) + + case tagRefCheckID: + refCheckCreatorID, err = binary.ReadUvarint(br) + if err == nil { + refCheckFileNum, err = binary.ReadUvarint(br) + } + + case tagLocator: + locator, err = decodeString(br) + + case tagCustomObjectName: + customObjName, err = decodeString(br) + + default: + // Ignore unknown tags, unless they're not safe to ignore. + if tag&tagNotSafeToIgnoreMask != 0 { + return decodedBacking{}, errors.Newf("unknown tag %d", tag) + } + var dataLen uint64 + dataLen, err = binary.ReadUvarint(br) + if err == nil { + _, err = br.Seek(int64(dataLen), io.SeekCurrent) + } + } + if err != nil { + return decodedBacking{}, err + } + } + if customObjName == "" { + if creatorID == 0 { + return decodedBacking{}, errors.Newf("remote object backing missing creator ID") + } + if creatorFileNum == 0 { + return decodedBacking{}, errors.Newf("remote object backing missing creator file num") + } + } + var res decodedBacking + res.meta.DiskFileNum = fileNum + res.meta.FileType = fileType + res.meta.Remote.CreatorID = objstorage.CreatorID(creatorID) + res.meta.Remote.CreatorFileNum = base.FileNum(creatorFileNum).DiskFileNum() + res.meta.Remote.CleanupMethod = objstorage.SharedCleanupMethod(cleanupMethod) + if res.meta.Remote.CleanupMethod == objstorage.SharedRefTracking { + if refCheckCreatorID == 0 || refCheckFileNum == 0 { + return decodedBacking{}, errors.Newf("remote object backing missing ref to check") + } + res.refToCheck.creatorID = objstorage.CreatorID(refCheckCreatorID) + res.refToCheck.fileNum = base.FileNum(refCheckFileNum).DiskFileNum() + } + res.meta.Remote.Locator = remote.Locator(locator) + res.meta.Remote.CustomObjectName = customObjName + return res, nil +} + +func encodeString(buf []byte, s string) []byte { + buf = binary.AppendUvarint(buf, uint64(len(s))) + buf = append(buf, []byte(s)...) + return buf +} + +func decodeString(br io.ByteReader) (string, error) { + length, err := binary.ReadUvarint(br) + if err != nil || length == 0 { + return "", err + } + buf := make([]byte, length) + for i := range buf { + buf[i], err = br.ReadByte() + if err != nil { + return "", err + } + } + return string(buf), nil +} + +// AttachRemoteObjects is part of the objstorage.Provider interface. +func (p *provider) AttachRemoteObjects( + objs []objstorage.RemoteObjectToAttach, +) ([]objstorage.ObjectMetadata, error) { + decoded := make([]decodedBacking, len(objs)) + for i, o := range objs { + var err error + decoded[i], err = decodeRemoteObjectBacking(o.FileType, o.FileNum, o.Backing) + if err != nil { + return nil, err + } + decoded[i].meta.Remote.Storage, err = p.ensureStorage(decoded[i].meta.Remote.Locator) + if err != nil { + return nil, err + } + } + + // Create the reference marker objects. + // TODO(radu): parallelize this. + for _, d := range decoded { + if d.meta.Remote.CleanupMethod != objstorage.SharedRefTracking { + continue + } + if err := p.sharedCreateRef(d.meta); err != nil { + // TODO(radu): clean up references previously created in this loop. + return nil, err + } + // Check the "origin"'s reference. + refName := sharedObjectRefName(d.meta, d.refToCheck.creatorID, d.refToCheck.fileNum) + if _, err := d.meta.Remote.Storage.Size(refName); err != nil { + _ = p.sharedUnref(d.meta) + // TODO(radu): clean up references previously created in this loop. + if d.meta.Remote.Storage.IsNotExistError(err) { + return nil, errors.Errorf("origin marker object %q does not exist;"+ + " object probably removed from the provider which created the backing", refName) + } + return nil, errors.Wrapf(err, "checking origin's marker object %s", refName) + } + } + + func() { + p.mu.Lock() + defer p.mu.Unlock() + for _, d := range decoded { + p.mu.remote.catalogBatch.AddObject(remoteobjcat.RemoteObjectMetadata{ + FileNum: d.meta.DiskFileNum, + FileType: d.meta.FileType, + CreatorID: d.meta.Remote.CreatorID, + CreatorFileNum: d.meta.Remote.CreatorFileNum, + CleanupMethod: d.meta.Remote.CleanupMethod, + Locator: d.meta.Remote.Locator, + }) + } + }() + if err := p.sharedSync(); err != nil { + return nil, err + } + + metas := make([]objstorage.ObjectMetadata, len(decoded)) + for i, d := range decoded { + metas[i] = d.meta + } + + p.mu.Lock() + defer p.mu.Unlock() + for _, meta := range metas { + p.mu.knownObjects[meta.DiskFileNum] = meta + } + return metas, nil +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_obj_name.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_obj_name.go new file mode 100644 index 0000000..b33a908 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_obj_name.go @@ -0,0 +1,91 @@ +// Copyright 2023 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 objstorageprovider + +import ( + "fmt" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" +) + +// remoteObjectName returns the name of an object on remote storage. +// +// For sstables, the format is: --.sst +// For example: 1a3f-2-000001.sst +func remoteObjectName(meta objstorage.ObjectMetadata) string { + if meta.Remote.CustomObjectName != "" { + return meta.Remote.CustomObjectName + } + switch meta.FileType { + case base.FileTypeTable: + return fmt.Sprintf( + "%04x-%d-%06d.sst", + objHash(meta), meta.Remote.CreatorID, meta.Remote.CreatorFileNum.FileNum(), + ) + } + panic("unknown FileType") +} + +// sharedObjectRefName returns the name of the object's ref marker associated +// with a given referencing provider. This name is the object's name concatenated with +// ".ref..". +// +// For example: 1a3f-2-000001.sst.ref.5.000008 +func sharedObjectRefName( + meta objstorage.ObjectMetadata, refCreatorID objstorage.CreatorID, refFileNum base.DiskFileNum, +) string { + if meta.Remote.CleanupMethod != objstorage.SharedRefTracking { + panic("ref object used when ref tracking disabled") + } + if meta.Remote.CustomObjectName != "" { + return fmt.Sprintf( + "%s.ref.%d.%06d", meta.Remote.CustomObjectName, refCreatorID, refFileNum.FileNum(), + ) + } + switch meta.FileType { + case base.FileTypeTable: + return fmt.Sprintf( + "%04x-%d-%06d.sst.ref.%d.%06d", + objHash(meta), meta.Remote.CreatorID, meta.Remote.CreatorFileNum.FileNum(), refCreatorID, refFileNum.FileNum(), + ) + } + panic("unknown FileType") +} + +func sharedObjectRefPrefix(meta objstorage.ObjectMetadata) string { + if meta.Remote.CustomObjectName != "" { + return meta.Remote.CustomObjectName + ".ref." + } + switch meta.FileType { + case base.FileTypeTable: + return fmt.Sprintf( + "%04x-%d-%06d.sst.ref.", + objHash(meta), meta.Remote.CreatorID, meta.Remote.CreatorFileNum.FileNum(), + ) + } + panic("unknown FileType") +} + +// sharedObjectRefName returns the name of the object's ref marker associated +// with this provider. This name is the object's name concatenated with +// ".ref..". +// +// For example: 1a3f-2-000001.sst.ref.5.000008 +func (p *provider) sharedObjectRefName(meta objstorage.ObjectMetadata) string { + if meta.Remote.CleanupMethod != objstorage.SharedRefTracking { + panic("ref object used when ref tracking disabled") + } + return sharedObjectRefName(meta, p.remote.shared.creatorID, meta.DiskFileNum) +} + +// objHash returns a 16-bit hash value derived from the creator ID and creator +// file num. We prepend this value to object names to ensure balanced +// partitioning with AWS (and likely other blob storage providers). +func objHash(meta objstorage.ObjectMetadata) uint16 { + const prime1 = 7459 + const prime2 = 17539 + return uint16(uint64(meta.Remote.CreatorID)*prime1 + uint64(meta.Remote.CreatorFileNum.FileNum())*prime2) +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_readable.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_readable.go new file mode 100644 index 0000000..991a1ba --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remote_readable.go @@ -0,0 +1,162 @@ +// Copyright 2023 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 objstorageprovider + +import ( + "context" + "io" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache" + "github.com/cockroachdb/pebble/objstorage/remote" +) + +const remoteMaxReadaheadSize = 1024 * 1024 /* 1MB */ + +// remoteReadable is a very simple implementation of Readable on top of the +// ReadCloser returned by remote.Storage.CreateObject. +type remoteReadable struct { + objReader remote.ObjectReader + size int64 + fileNum base.DiskFileNum + provider *provider +} + +var _ objstorage.Readable = (*remoteReadable)(nil) + +func (p *provider) newRemoteReadable( + objReader remote.ObjectReader, size int64, fileNum base.DiskFileNum, +) *remoteReadable { + return &remoteReadable{ + objReader: objReader, + size: size, + fileNum: fileNum, + provider: p, + } +} + +// ReadAt is part of the objstorage.Readable interface. +func (r *remoteReadable) ReadAt(ctx context.Context, p []byte, offset int64) error { + return r.readInternal(ctx, p, offset, false /* forCompaction */) +} + +// readInternal performs a read for the object, using the cache when +// appropriate. +func (r *remoteReadable) readInternal( + ctx context.Context, p []byte, offset int64, forCompaction bool, +) error { + if cache := r.provider.remote.cache; cache != nil { + flags := sharedcache.ReadFlags{ + // Don't add data to the cache if this read is for a compaction. + ReadOnly: forCompaction, + } + return r.provider.remote.cache.ReadAt(ctx, r.fileNum, p, offset, r.objReader, r.size, flags) + } + return r.objReader.ReadAt(ctx, p, offset) +} + +func (r *remoteReadable) Close() error { + defer func() { r.objReader = nil }() + return r.objReader.Close() +} + +func (r *remoteReadable) Size() int64 { + return r.size +} + +func (r *remoteReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle { + // TODO(radu): use a pool. + rh := &remoteReadHandle{readable: r} + rh.readahead.state = makeReadaheadState(remoteMaxReadaheadSize) + return rh +} + +type remoteReadHandle struct { + readable *remoteReadable + readahead struct { + state readaheadState + data []byte + offset int64 + } + forCompaction bool +} + +var _ objstorage.ReadHandle = (*remoteReadHandle)(nil) + +// ReadAt is part of the objstorage.ReadHandle interface. +func (r *remoteReadHandle) ReadAt(ctx context.Context, p []byte, offset int64) error { + readaheadSize := r.maybeReadahead(offset, len(p)) + + // Check if we already have the data from a previous read-ahead. + if rhSize := int64(len(r.readahead.data)); rhSize > 0 { + if r.readahead.offset <= offset && r.readahead.offset+rhSize > offset { + n := copy(p, r.readahead.data[offset-r.readahead.offset:]) + if n == len(p) { + // All data was available. + return nil + } + // Use the data that we had and do a shorter read. + offset += int64(n) + p = p[n:] + readaheadSize -= n + } + } + + if readaheadSize > len(p) { + // Don't try to read past EOF. + if offset+int64(readaheadSize) > r.readable.size { + readaheadSize = int(r.readable.size - offset) + if readaheadSize <= 0 { + // This shouldn't happen in practice (Pebble should never try to read + // past EOF). + return io.EOF + } + } + r.readahead.offset = offset + // TODO(radu): we need to somehow account for this memory. + if cap(r.readahead.data) >= readaheadSize { + r.readahead.data = r.readahead.data[:readaheadSize] + } else { + r.readahead.data = make([]byte, readaheadSize) + } + + if err := r.readable.readInternal(ctx, r.readahead.data, offset, r.forCompaction); err != nil { + // Make sure we don't treat the data as valid next time. + r.readahead.data = r.readahead.data[:0] + return err + } + copy(p, r.readahead.data) + return nil + } + + return r.readable.readInternal(ctx, p, offset, r.forCompaction) +} + +func (r *remoteReadHandle) maybeReadahead(offset int64, len int) int { + if r.forCompaction { + return remoteMaxReadaheadSize + } + return int(r.readahead.state.maybeReadahead(offset, int64(len))) +} + +// Close is part of the objstorage.ReadHandle interface. +func (r *remoteReadHandle) Close() error { + r.readable = nil + r.readahead.data = nil + return nil +} + +// SetupForCompaction is part of the objstorage.ReadHandle interface. +func (r *remoteReadHandle) SetupForCompaction() { + r.forCompaction = true +} + +// RecordCacheHit is part of the objstorage.ReadHandle interface. +func (r *remoteReadHandle) RecordCacheHit(_ context.Context, offset, size int64) { + if !r.forCompaction { + r.readahead.state.recordCacheHit(offset, size) + } +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/sharedobjcat/catalog.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat/catalog.go similarity index 73% rename from vendor/github.com/cockroachdb/pebble/objstorage/sharedobjcat/catalog.go rename to vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat/catalog.go index 99d9c8b..8508f19 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/sharedobjcat/catalog.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat/catalog.go @@ -2,7 +2,7 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -package sharedobjcat +package remoteobjcat import ( "fmt" @@ -12,23 +12,25 @@ import ( "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/remote" "github.com/cockroachdb/pebble/record" "github.com/cockroachdb/pebble/vfs" "github.com/cockroachdb/pebble/vfs/atomicfs" ) -// Catalog is used to manage the on-disk shared object catalog. +// Catalog is used to manage the on-disk remote object catalog. // // The catalog file is a log of records, where each record is an encoded -// versionEdit. +// VersionEdit. type Catalog struct { fs vfs.FS dirname string mu struct { sync.Mutex - creatorID CreatorID - objects map[base.FileNum]SharedObjectMetadata + creatorID objstorage.CreatorID + objects map[base.DiskFileNum]RemoteObjectMetadata marker *atomicfs.Marker @@ -43,44 +45,41 @@ type Catalog struct { } } -// CreatorID identifies the DB instance that originally created a shared object. -// This ID is incorporated in backing object names. -// Must be non-zero. -type CreatorID uint64 - -// IsSet returns true if the CreatorID is not zero. -func (c CreatorID) IsSet() bool { return c != 0 } - -func (c CreatorID) String() string { return fmt.Sprintf("%020d", c) } - -// SharedObjectMetadata encapsulates the data stored in the catalog file for each object. -type SharedObjectMetadata struct { +// RemoteObjectMetadata encapsulates the data stored in the catalog file for each object. +type RemoteObjectMetadata struct { // FileNum is the identifier for the object within the context of a single DB // instance. - FileNum base.FileNum + FileNum base.DiskFileNum // FileType is the type of the object. Only certain FileTypes are possible. FileType base.FileType // CreatorID identifies the DB instance that originally created the object. - CreatorID CreatorID + CreatorID objstorage.CreatorID // CreatorFileNum is the identifier for the object within the context of the // DB instance that originally created the object. - CreatorFileNum base.FileNum + CreatorFileNum base.DiskFileNum + // CleanupMethod indicates the method for cleaning up unused shared objects. + CleanupMethod objstorage.SharedCleanupMethod + // Locator identifies a remote.Storage implementation. + Locator remote.Locator + // CustomObjectName (if it is set) overrides the object name that is normally + // derived from the CreatorID and CreatorFileNum. + CustomObjectName string } const ( - catalogFilenameBase = "SHARED-CATALOG" - catalogMarkerName = "shared-catalog" + catalogFilenameBase = "REMOTE-OBJ-CATALOG" + catalogMarkerName = "remote-obj-catalog" // We create a new file when the size exceeds 1MB (and some other conditions // hold; see record.RotationHelper). rotateFileSize = 1024 * 1024 // 1MB ) -// CatalogContents contains the shared objects in the catalog. +// CatalogContents contains the remote objects in the catalog. type CatalogContents struct { // CreatorID, if it is set. - CreatorID CreatorID - Objects []SharedObjectMetadata + CreatorID objstorage.CreatorID + Objects []RemoteObjectMetadata } // Open creates a Catalog and loads any existing catalog file, returning the @@ -90,7 +89,7 @@ func Open(fs vfs.FS, dirname string) (*Catalog, CatalogContents, error) { fs: fs, dirname: dirname, } - c.mu.objects = make(map[base.FileNum]SharedObjectMetadata) + c.mu.objects = make(map[base.DiskFileNum]RemoteObjectMetadata) var err error c.mu.marker, c.mu.catalogFilename, err = atomicfs.LocateMarker(fs, dirname, catalogMarkerName) @@ -109,20 +108,20 @@ func Open(fs vfs.FS, dirname string) (*Catalog, CatalogContents, error) { } res := CatalogContents{ CreatorID: c.mu.creatorID, - Objects: make([]SharedObjectMetadata, 0, len(c.mu.objects)), + Objects: make([]RemoteObjectMetadata, 0, len(c.mu.objects)), } for _, meta := range c.mu.objects { res.Objects = append(res.Objects, meta) } // Sort the objects so the function is deterministic. sort.Slice(res.Objects, func(i, j int) bool { - return res.Objects[i].FileNum < res.Objects[j].FileNum + return res.Objects[i].FileNum.FileNum() < res.Objects[j].FileNum.FileNum() }) return c, res, nil } // SetCreatorID sets the creator ID. If it is already set, it must match. -func (c *Catalog) SetCreatorID(id CreatorID) error { +func (c *Catalog) SetCreatorID(id objstorage.CreatorID) error { if !id.IsSet() { return errors.AssertionFailedf("attempt to unset CreatorID") } @@ -137,9 +136,9 @@ func (c *Catalog) SetCreatorID(id CreatorID) error { return nil } - ve := versionEdit{CreatorID: id} + ve := VersionEdit{CreatorID: id} if err := c.writeToCatalogFileLocked(&ve); err != nil { - return errors.Wrapf(err, "pebble: could not write to shared object catalog: %v", err) + return errors.Wrapf(err, "pebble: could not write to remote object catalog: %v", err) } c.mu.creatorID = id return nil @@ -166,19 +165,19 @@ func (c *Catalog) closeCatalogFile() error { // Batch is used to perform multiple object additions/deletions at once. type Batch struct { - ve versionEdit + ve VersionEdit } // AddObject adds a new object to the batch. // // The given FileNum must be new - it must not match that of any object that was // ever in the catalog. -func (b *Batch) AddObject(meta SharedObjectMetadata) { +func (b *Batch) AddObject(meta RemoteObjectMetadata) { b.ve.NewObjects = append(b.ve.NewObjects, meta) } // DeleteObject adds an object removal to the batch. -func (b *Batch) DeleteObject(fileNum base.FileNum) { +func (b *Batch) DeleteObject(fileNum base.DiskFileNum) { b.ve.DeletedObjects = append(b.ve.DeletedObjects, fileNum) } @@ -197,11 +196,11 @@ func (b *Batch) IsEmpty() bool { func (b *Batch) Copy() Batch { var res Batch if len(b.ve.NewObjects) > 0 { - res.ve.NewObjects = make([]SharedObjectMetadata, len(b.ve.NewObjects)) + res.ve.NewObjects = make([]RemoteObjectMetadata, len(b.ve.NewObjects)) copy(res.ve.NewObjects, b.ve.NewObjects) } if len(b.ve.DeletedObjects) > 0 { - res.ve.DeletedObjects = make([]base.FileNum, len(b.ve.DeletedObjects)) + res.ve.DeletedObjects = make([]base.DiskFileNum, len(b.ve.DeletedObjects)) copy(res.ve.DeletedObjects, b.ve.DeletedObjects) } return res @@ -219,29 +218,40 @@ func (c *Catalog) ApplyBatch(b Batch) error { c.mu.Lock() defer c.mu.Unlock() - for _, n := range b.ve.DeletedObjects { - if _, exists := c.mu.objects[n]; !exists { - return errors.AssertionFailedf("deleting non-existent object %s", n) + // Sanity checks. + toAdd := make(map[base.DiskFileNum]struct{}, len(b.ve.NewObjects)) + exists := func(n base.DiskFileNum) bool { + _, ok := c.mu.objects[n] + if !ok { + _, ok = toAdd[n] } + return ok } for _, meta := range b.ve.NewObjects { - if _, exists := c.mu.objects[meta.FileNum]; exists { + if exists(meta.FileNum) { return errors.AssertionFailedf("adding existing object %s", meta.FileNum) } + toAdd[meta.FileNum] = struct{}{} + } + for _, n := range b.ve.DeletedObjects { + if !exists(n) { + return errors.AssertionFailedf("deleting non-existent object %s", n) + } } if err := c.writeToCatalogFileLocked(&b.ve); err != nil { - return errors.Wrapf(err, "pebble: could not write to shared object catalog: %v", err) + return errors.Wrapf(err, "pebble: could not write to remote object catalog: %v", err) } - // Apply the batch to our current state. - for _, n := range b.ve.DeletedObjects { - delete(c.mu.objects, n) - } + // Add new objects before deleting any objects. This allows for cases where + // the same batch adds and deletes an object. for _, meta := range b.ve.NewObjects { c.mu.objects[meta.FileNum] = meta } - b.Reset() + for _, n := range b.ve.DeletedObjects { + delete(c.mu.objects, n) + } + return nil } @@ -250,7 +260,7 @@ func (c *Catalog) loadFromCatalogFile(filename string) error { f, err := c.fs.Open(catalogPath) if err != nil { return errors.Wrapf( - err, "pebble: could not open shared object catalog file %q for DB %q", + err, "pebble: could not open remote object catalog file %q for DB %q", errors.Safe(filename), c.dirname, ) } @@ -262,32 +272,26 @@ func (c *Catalog) loadFromCatalogFile(filename string) error { break } if err != nil { - return errors.Wrapf(err, "pebble: error when loading shared object catalog file %q", + return errors.Wrapf(err, "pebble: error when loading remote object catalog file %q", errors.Safe(filename)) } - var ve versionEdit - err = ve.Decode(r) - if err != nil { - return errors.Wrapf(err, "pebble: error when loading shared object catalog file %q", + var ve VersionEdit + if err := ve.Decode(r); err != nil { + return errors.Wrapf(err, "pebble: error when loading remote object catalog file %q", errors.Safe(filename)) } // Apply the version edit to the current state. - if ve.CreatorID.IsSet() { - c.mu.creatorID = ve.CreatorID - } - for _, fileNum := range ve.DeletedObjects { - delete(c.mu.objects, fileNum) - } - for _, meta := range ve.NewObjects { - c.mu.objects[meta.FileNum] = meta + if err := ve.Apply(&c.mu.creatorID, c.mu.objects); err != nil { + return errors.Wrapf(err, "pebble: error when loading remote object catalog file %q", + errors.Safe(filename)) } } return nil } -// writeToCatalogFileLocked writes a versionEdit to the catalog file. +// writeToCatalogFileLocked writes a VersionEdit to the catalog file. // Creates a new file if this is the first write. -func (c *Catalog) writeToCatalogFileLocked(ve *versionEdit) error { +func (c *Catalog) writeToCatalogFileLocked(ve *VersionEdit) error { c.mu.rotationHelper.AddRecord(int64(len(ve.NewObjects) + len(ve.DeletedObjects))) snapshotSize := int64(len(c.mu.objects)) @@ -330,9 +334,10 @@ func (c *Catalog) createNewCatalogFileLocked() (outErr error) { } recWriter := record.NewWriter(file) err = func() error { - // Create a versionEdit that gets us from an empty catalog to the current state. - var ve versionEdit - ve.NewObjects = make([]SharedObjectMetadata, 0, len(c.mu.objects)) + // Create a VersionEdit that gets us from an empty catalog to the current state. + var ve VersionEdit + ve.CreatorID = c.mu.creatorID + ve.NewObjects = make([]RemoteObjectMetadata, 0, len(c.mu.objects)) for _, meta := range c.mu.objects { ve.NewObjects = append(ve.NewObjects, meta) } @@ -367,7 +372,7 @@ func (c *Catalog) createNewCatalogFileLocked() (outErr error) { return nil } -func writeRecord(ve *versionEdit, file vfs.File, recWriter *record.Writer) error { +func writeRecord(ve *VersionEdit, file vfs.File, recWriter *record.Writer) error { w, err := recWriter.Next() if err != nil { return err diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit.go new file mode 100644 index 0000000..44552f5 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat/version_edit.go @@ -0,0 +1,254 @@ +// Copyright 2023 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 remoteobjcat + +import ( + "bufio" + "encoding/binary" + "io" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/remote" +) + +// VersionEdit is a modification to the remote object state which can be encoded +// into a record. +// +// TODO(radu): consider adding creation and deletion time for debugging purposes. +type VersionEdit struct { + NewObjects []RemoteObjectMetadata + DeletedObjects []base.DiskFileNum + CreatorID objstorage.CreatorID +} + +const ( + // tagNewObject is followed by the FileNum, creator ID, creator FileNum, + // cleanup method, optional new object tags, and ending with a 0 byte. + tagNewObject = 1 + // tagDeletedObject is followed by the FileNum. + tagDeletedObject = 2 + // tagCreatorID is followed by the Creator ID for this store. This ID can + // never change. + tagCreatorID = 3 + // tagNewObjectLocator is an optional tag inside the tagNewObject payload. It + // is followed by the encoded length of the locator string and the string. + tagNewObjectLocator = 4 + // tagNewObjectCustomName is an optional tag inside the tagNewObject payload. + // It is followed by the encoded length of the custom object name string + // followed by the string. + tagNewObjectCustomName = 5 +) + +// Object type values. We don't want to encode FileType directly because it is +// more general (and we want freedom to change it in the future). +const ( + objTypeTable = 1 +) + +func objTypeToFileType(objType uint64) (base.FileType, error) { + switch objType { + case objTypeTable: + return base.FileTypeTable, nil + default: + return 0, errors.Newf("unknown object type %d", objType) + } +} + +func fileTypeToObjType(fileType base.FileType) (uint64, error) { + switch fileType { + case base.FileTypeTable: + return objTypeTable, nil + + default: + return 0, errors.Newf("unknown object type for file type %d", fileType) + } +} + +// Encode encodes an edit to the specified writer. +func (v *VersionEdit) Encode(w io.Writer) error { + buf := make([]byte, 0, binary.MaxVarintLen64*(len(v.NewObjects)*10+len(v.DeletedObjects)*2+2)) + for _, meta := range v.NewObjects { + objType, err := fileTypeToObjType(meta.FileType) + if err != nil { + return err + } + buf = binary.AppendUvarint(buf, uint64(tagNewObject)) + buf = binary.AppendUvarint(buf, uint64(meta.FileNum.FileNum())) + buf = binary.AppendUvarint(buf, objType) + buf = binary.AppendUvarint(buf, uint64(meta.CreatorID)) + buf = binary.AppendUvarint(buf, uint64(meta.CreatorFileNum.FileNum())) + buf = binary.AppendUvarint(buf, uint64(meta.CleanupMethod)) + if meta.Locator != "" { + buf = binary.AppendUvarint(buf, uint64(tagNewObjectLocator)) + buf = encodeString(buf, string(meta.Locator)) + } + if meta.CustomObjectName != "" { + buf = binary.AppendUvarint(buf, uint64(tagNewObjectCustomName)) + buf = encodeString(buf, meta.CustomObjectName) + } + // Append 0 as the terminator for optional new object tags. + buf = binary.AppendUvarint(buf, 0) + } + + for _, dfn := range v.DeletedObjects { + buf = binary.AppendUvarint(buf, uint64(tagDeletedObject)) + buf = binary.AppendUvarint(buf, uint64(dfn.FileNum())) + } + if v.CreatorID.IsSet() { + buf = binary.AppendUvarint(buf, uint64(tagCreatorID)) + buf = binary.AppendUvarint(buf, uint64(v.CreatorID)) + } + _, err := w.Write(buf) + return err +} + +// Decode decodes an edit from the specified reader. +func (v *VersionEdit) Decode(r io.Reader) error { + br, ok := r.(io.ByteReader) + if !ok { + br = bufio.NewReader(r) + } + for { + tag, err := binary.ReadUvarint(br) + if err == io.EOF { + break + } + if err != nil { + return err + } + + err = nil + switch tag { + case tagNewObject: + var fileNum, creatorID, creatorFileNum, cleanupMethod uint64 + var locator, customName string + var fileType base.FileType + fileNum, err = binary.ReadUvarint(br) + if err == nil { + var objType uint64 + objType, err = binary.ReadUvarint(br) + if err == nil { + fileType, err = objTypeToFileType(objType) + } + } + if err == nil { + creatorID, err = binary.ReadUvarint(br) + } + if err == nil { + creatorFileNum, err = binary.ReadUvarint(br) + } + if err == nil { + cleanupMethod, err = binary.ReadUvarint(br) + } + for err == nil { + var optionalTag uint64 + optionalTag, err = binary.ReadUvarint(br) + if err != nil || optionalTag == 0 { + break + } + + switch optionalTag { + case tagNewObjectLocator: + locator, err = decodeString(br) + + case tagNewObjectCustomName: + customName, err = decodeString(br) + + default: + err = errors.Newf("unknown newObject tag %d", optionalTag) + } + } + + if err == nil { + v.NewObjects = append(v.NewObjects, RemoteObjectMetadata{ + FileNum: base.FileNum(fileNum).DiskFileNum(), + FileType: fileType, + CreatorID: objstorage.CreatorID(creatorID), + CreatorFileNum: base.FileNum(creatorFileNum).DiskFileNum(), + CleanupMethod: objstorage.SharedCleanupMethod(cleanupMethod), + Locator: remote.Locator(locator), + CustomObjectName: customName, + }) + } + + case tagDeletedObject: + var fileNum uint64 + fileNum, err = binary.ReadUvarint(br) + if err == nil { + v.DeletedObjects = append(v.DeletedObjects, base.FileNum(fileNum).DiskFileNum()) + } + + case tagCreatorID: + var id uint64 + id, err = binary.ReadUvarint(br) + if err == nil { + v.CreatorID = objstorage.CreatorID(id) + } + + default: + err = errors.Newf("unknown tag %d", tag) + } + + if err != nil { + if err == io.EOF { + return errCorruptCatalog + } + return err + } + } + return nil +} + +func encodeString(buf []byte, s string) []byte { + buf = binary.AppendUvarint(buf, uint64(len(s))) + buf = append(buf, []byte(s)...) + return buf +} + +func decodeString(br io.ByteReader) (string, error) { + length, err := binary.ReadUvarint(br) + if err != nil || length == 0 { + return "", err + } + buf := make([]byte, length) + for i := range buf { + buf[i], err = br.ReadByte() + if err != nil { + return "", err + } + } + return string(buf), nil +} + +var errCorruptCatalog = base.CorruptionErrorf("pebble: corrupt remote object catalog") + +// Apply the version edit to a creator ID and a map of objects. +func (v *VersionEdit) Apply( + creatorID *objstorage.CreatorID, objects map[base.DiskFileNum]RemoteObjectMetadata, +) error { + if v.CreatorID.IsSet() { + *creatorID = v.CreatorID + } + for _, meta := range v.NewObjects { + if invariants.Enabled { + if _, exists := objects[meta.FileNum]; exists { + return errors.AssertionFailedf("version edit adds existing object %s", meta.FileNum) + } + } + objects[meta.FileNum] = meta + } + for _, fileNum := range v.DeletedObjects { + if invariants.Enabled { + if _, exists := objects[fileNum]; !exists { + return errors.AssertionFailedf("version edit deletes non-existent object %s", fileNum) + } + } + delete(objects, fileNum) + } + return nil +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared_writable.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared_writable.go new file mode 100644 index 0000000..5e8d45a --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/shared_writable.go @@ -0,0 +1,65 @@ +// Copyright 2023 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 objstorageprovider + +import ( + "io" + + "github.com/cockroachdb/pebble/objstorage" +) + +// NewRemoteWritable creates an objstorage.Writable out of an io.WriteCloser. +func NewRemoteWritable(obj io.WriteCloser) objstorage.Writable { + return &sharedWritable{storageWriter: obj} +} + +// sharedWritable is a very simple implementation of Writable on top of the +// WriteCloser returned by remote.Storage.CreateObject. +type sharedWritable struct { + // Either both p and meta must be unset / zero values, or both must be set. + // The case where both are unset is true only in tests. + p *provider + meta objstorage.ObjectMetadata + storageWriter io.WriteCloser +} + +var _ objstorage.Writable = (*sharedWritable)(nil) + +// Write is part of the Writable interface. +func (w *sharedWritable) Write(p []byte) error { + _, err := w.storageWriter.Write(p) + return err +} + +// Finish is part of the Writable interface. +func (w *sharedWritable) Finish() error { + err := w.storageWriter.Close() + w.storageWriter = nil + if err != nil { + w.Abort() + return err + } + + // Create the marker object. + if w.p != nil { + if err := w.p.sharedCreateRef(w.meta); err != nil { + w.Abort() + return err + } + } + return nil +} + +// Abort is part of the Writable interface. +func (w *sharedWritable) Abort() { + if w.storageWriter != nil { + _ = w.storageWriter.Close() + w.storageWriter = nil + } + if w.p != nil { + w.p.removeMetadata(w.meta.DiskFileNum) + } + // TODO(radu): delete the object if it was created. +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go new file mode 100644 index 0000000..6d6409e --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache/shared_cache.go @@ -0,0 +1,900 @@ +// Copyright 2023 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 sharedcache + +import ( + "context" + "fmt" + "io" + "math/bits" + "sync" + "sync/atomic" + "time" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/vfs" + "github.com/prometheus/client_golang/prometheus" +) + +// Exported to enable exporting from package pebble to enable +// exporting metrics with below buckets in CRDB. +var ( + IOBuckets = prometheus.ExponentialBucketsRange(float64(time.Millisecond*1), float64(10*time.Second), 50) + ChannelWriteBuckets = prometheus.ExponentialBucketsRange(float64(time.Microsecond*1), float64(10*time.Second), 50) +) + +// Cache is a persistent cache backed by a local filesystem. It is intended +// to cache data that is in slower shared storage (e.g. S3), hence the +// package name 'sharedcache'. +type Cache struct { + shards []shard + writeWorkers writeWorkers + + bm blockMath + shardingBlockSize int64 + + logger base.Logger + metrics internalMetrics +} + +// Metrics is a struct containing metrics exported by the secondary cache. +// TODO(josh): Reconsider the set of metrics exported by the secondary cache +// before we release the secondary cache to users. We choose to export many metrics +// right now, so we learn a lot from the benchmarking we are doing over the 23.2 +// cycle. +type Metrics struct { + // The number of sstable bytes stored in the cache. + Size int64 + // The count of cache blocks in the cache (not sstable blocks). + Count int64 + + // The number of calls to ReadAt. + TotalReads int64 + // The number of calls to ReadAt that require reading data from 2+ shards. + MultiShardReads int64 + // The number of calls to ReadAt that require reading data from 2+ cache blocks. + MultiBlockReads int64 + // The number of calls to ReadAt where all data returned was read from the cache. + ReadsWithFullHit int64 + // The number of calls to ReadAt where some data returned was read from the cache. + ReadsWithPartialHit int64 + // The number of calls to ReadAt where no data returned was read from the cache. + ReadsWithNoHit int64 + + // The number of times a cache block was evicted from the cache. + Evictions int64 + // The number of times writing a cache block to the cache failed. + WriteBackFailures int64 + + // The latency of calls to get some data from the cache. + GetLatency prometheus.Histogram + // The latency of reads of a single cache block from disk. + DiskReadLatency prometheus.Histogram + // The latency of writing data to write back to the cache to a channel. + // Generally should be low, but if the channel is full, could be high. + QueuePutLatency prometheus.Histogram + // The latency of calls to put some data read from block storage into the cache. + PutLatency prometheus.Histogram + // The latency of writes of a single cache block to disk. + DiskWriteLatency prometheus.Histogram +} + +// See docs at Metrics. +type internalMetrics struct { + count atomic.Int64 + + totalReads atomic.Int64 + multiShardReads atomic.Int64 + multiBlockReads atomic.Int64 + readsWithFullHit atomic.Int64 + readsWithPartialHit atomic.Int64 + readsWithNoHit atomic.Int64 + + evictions atomic.Int64 + writeBackFailures atomic.Int64 + + getLatency prometheus.Histogram + diskReadLatency prometheus.Histogram + queuePutLatency prometheus.Histogram + putLatency prometheus.Histogram + diskWriteLatency prometheus.Histogram +} + +const ( + // writeWorkersPerShard is used to establish the number of worker goroutines + // that perform writes to the cache. + writeWorkersPerShard = 4 + // writeTaskPerWorker is used to establish how many tasks can be queued up + // until we have to block. + writeTasksPerWorker = 4 +) + +// Open opens a cache. If there is no existing cache at fsDir, a new one +// is created. +func Open( + fs vfs.FS, + logger base.Logger, + fsDir string, + blockSize int, + // shardingBlockSize is the size of a shard block. The cache is split into contiguous + // shardingBlockSize units. The units are distributed across multiple independent shards + // of the cache, via a hash(offset) modulo num shards operation. The cache replacement + // policies operate at the level of shard, not whole cache. This is done to reduce lock + // contention. + shardingBlockSize int64, + sizeBytes int64, + numShards int, +) (*Cache, error) { + if minSize := shardingBlockSize * int64(numShards); sizeBytes < minSize { + // Up the size so that we have one block per shard. In practice, this should + // only happen in tests. + sizeBytes = minSize + } + + c := &Cache{ + logger: logger, + bm: makeBlockMath(blockSize), + shardingBlockSize: shardingBlockSize, + } + c.shards = make([]shard, numShards) + blocksPerShard := sizeBytes / int64(numShards) / int64(blockSize) + for i := range c.shards { + if err := c.shards[i].init(c, fs, fsDir, i, blocksPerShard, blockSize, shardingBlockSize); err != nil { + return nil, err + } + } + + c.writeWorkers.Start(c, numShards*writeWorkersPerShard) + + c.metrics.getLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets}) + c.metrics.diskReadLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets}) + c.metrics.putLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets}) + c.metrics.diskWriteLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: IOBuckets}) + + // Measures a channel write, so lower min. + c.metrics.queuePutLatency = prometheus.NewHistogram(prometheus.HistogramOpts{Buckets: ChannelWriteBuckets}) + + return c, nil +} + +// Close closes the cache. Methods such as ReadAt should not be called after Close is +// called. +func (c *Cache) Close() error { + c.writeWorkers.Stop() + + var retErr error + for i := range c.shards { + if err := c.shards[i].close(); err != nil && retErr == nil { + retErr = err + } + } + c.shards = nil + return retErr +} + +// Metrics return metrics for the cache. Callers should not mutate +// the returned histograms, which are pointer types. +func (c *Cache) Metrics() Metrics { + return Metrics{ + Count: c.metrics.count.Load(), + Size: c.metrics.count.Load() * int64(c.bm.BlockSize()), + TotalReads: c.metrics.totalReads.Load(), + MultiShardReads: c.metrics.multiShardReads.Load(), + MultiBlockReads: c.metrics.multiBlockReads.Load(), + ReadsWithFullHit: c.metrics.readsWithFullHit.Load(), + ReadsWithPartialHit: c.metrics.readsWithPartialHit.Load(), + ReadsWithNoHit: c.metrics.readsWithNoHit.Load(), + Evictions: c.metrics.evictions.Load(), + WriteBackFailures: c.metrics.writeBackFailures.Load(), + GetLatency: c.metrics.getLatency, + DiskReadLatency: c.metrics.diskReadLatency, + QueuePutLatency: c.metrics.queuePutLatency, + PutLatency: c.metrics.putLatency, + DiskWriteLatency: c.metrics.diskWriteLatency, + } +} + +// ReadFlags contains options for Cache.ReadAt. +type ReadFlags struct { + // ReadOnly instructs ReadAt to not write any new data into the cache; it is + // used when the data is unlikely to be used again. + ReadOnly bool +} + +// ReadAt performs a read form an object, attempting to use cached data when +// possible. +func (c *Cache) ReadAt( + ctx context.Context, + fileNum base.DiskFileNum, + p []byte, + ofs int64, + objReader remote.ObjectReader, + objSize int64, + flags ReadFlags, +) error { + c.metrics.totalReads.Add(1) + if ofs >= objSize { + if invariants.Enabled { + panic(fmt.Sprintf("invalid ReadAt offset %v %v", ofs, objSize)) + } + return io.EOF + } + // TODO(radu): for compaction reads, we may not want to read from the cache at + // all. + { + start := time.Now() + n, err := c.get(fileNum, p, ofs) + c.metrics.getLatency.Observe(float64(time.Since(start))) + if err != nil { + return err + } + if n == len(p) { + // Everything was in cache! + c.metrics.readsWithFullHit.Add(1) + return nil + } + if n == 0 { + c.metrics.readsWithNoHit.Add(1) + } else { + c.metrics.readsWithPartialHit.Add(1) + } + + // Note this. The below code does not need the original ofs, as with the earlier + // reading from the cache done, the relevant offset is ofs + int64(n). Same with p. + ofs += int64(n) + p = p[n:] + + if invariants.Enabled { + if n != 0 && c.bm.Remainder(ofs) != 0 { + panic(fmt.Sprintf("after non-zero read from cache, ofs is not block-aligned: %v %v", ofs, n)) + } + } + } + + if flags.ReadOnly { + return objReader.ReadAt(ctx, p, ofs) + } + + // We must do reads with offset & size that are multiples of the block size. Else + // later cache hits may return incorrect zeroed results from the cache. + firstBlockInd := c.bm.Block(ofs) + adjustedOfs := c.bm.BlockOffset(firstBlockInd) + + // Take the length of what is left to read plus the length of the adjustment of + // the offset plus the size of a block minus one and divide by the size of a block + // to get the number of blocks to read from the object. + sizeOfOffAdjustment := int(ofs - adjustedOfs) + adjustedLen := int(c.bm.RoundUp(int64(len(p) + sizeOfOffAdjustment))) + adjustedP := make([]byte, adjustedLen) + + // Read the rest from the object. We may need to cap the length to avoid past EOF reads. + eofCap := int64(adjustedLen) + if adjustedOfs+eofCap > objSize { + eofCap = objSize - adjustedOfs + } + if err := objReader.ReadAt(ctx, adjustedP[:eofCap], adjustedOfs); err != nil { + return err + } + copy(p, adjustedP[sizeOfOffAdjustment:]) + + start := time.Now() + c.writeWorkers.QueueWrite(fileNum, adjustedP, adjustedOfs) + c.metrics.queuePutLatency.Observe(float64(time.Since(start))) + + return nil +} + +// get attempts to read the requested data from the cache, if it is already +// there. +// +// If all data is available, returns n = len(p). +// +// If data is partially available, a prefix of the data is read; returns n < len(p) +// and no error. If no prefix is available, returns n = 0 and no error. +func (c *Cache) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) { + // The data extent might cross shard boundaries, hence the loop. In the hot + // path, max two iterations of this loop will be executed, since reads are sized + // in units of sstable block size. + var multiShard bool + for { + shard := c.getShard(fileNum, ofs+int64(n)) + cappedLen := len(p[n:]) + if toBoundary := int(c.shardingBlockSize - ((ofs + int64(n)) % c.shardingBlockSize)); cappedLen > toBoundary { + cappedLen = toBoundary + } + numRead, err := shard.get(fileNum, p[n:n+cappedLen], ofs+int64(n)) + if err != nil { + return n, err + } + n += numRead + if numRead < cappedLen { + // We only read a prefix from this shard. + return n, nil + } + if n == len(p) { + // We are done. + return n, nil + } + // Data extent crosses shard boundary, continue with next shard. + if !multiShard { + c.metrics.multiShardReads.Add(1) + multiShard = true + } + } +} + +// set attempts to write the requested data to the cache. Both ofs & len(p) must +// be multiples of the block size. +// +// If all of p is not written to the shard, set returns a non-nil error. +func (c *Cache) set(fileNum base.DiskFileNum, p []byte, ofs int64) error { + if invariants.Enabled { + if c.bm.Remainder(ofs) != 0 || c.bm.Remainder(int64(len(p))) != 0 { + panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p))) + } + } + + // The data extent might cross shard boundaries, hence the loop. In the hot + // path, max two iterations of this loop will be executed, since reads are sized + // in units of sstable block size. + n := 0 + for { + shard := c.getShard(fileNum, ofs+int64(n)) + cappedLen := len(p[n:]) + if toBoundary := int(c.shardingBlockSize - ((ofs + int64(n)) % c.shardingBlockSize)); cappedLen > toBoundary { + cappedLen = toBoundary + } + err := shard.set(fileNum, p[n:n+cappedLen], ofs+int64(n)) + if err != nil { + return err + } + // set returns an error if cappedLen bytes aren't written to the shard. + n += cappedLen + if n == len(p) { + // We are done. + return nil + } + // Data extent crosses shard boundary, continue with next shard. + } +} + +func (c *Cache) getShard(fileNum base.DiskFileNum, ofs int64) *shard { + const prime64 = 1099511628211 + hash := uint64(fileNum.FileNum())*prime64 + uint64(ofs/c.shardingBlockSize) + // TODO(josh): Instance change ops are often run in production. Such an operation + // updates len(c.shards); see openSharedCache. As a result, the behavior of this + // function changes, and the cache empties out at restart time. We may want a better + // story here eventually. + return &c.shards[hash%uint64(len(c.shards))] +} + +type shard struct { + cache *Cache + file vfs.File + sizeInBlocks int64 + bm blockMath + shardingBlockSize int64 + mu struct { + sync.Mutex + // TODO(josh): None of these datastructures are space-efficient. + // Focusing on correctness to start. + where whereMap + blocks []cacheBlockState + // Head of LRU list (doubly-linked circular). + lruHead cacheBlockIndex + // Head of free list (singly-linked chain). + freeHead cacheBlockIndex + } +} + +type cacheBlockState struct { + lock lockState + logical logicalBlockID + + // next is the next block in the LRU or free list (or invalidBlockIndex if it + // is the last block in the free list). + next cacheBlockIndex + + // prev is the previous block in the LRU list. It is not used when the block + // is in the free list. + prev cacheBlockIndex +} + +// Maps a logical block in an SST to an index of the cache block with the +// file contents (to the "cache block index"). +type whereMap map[logicalBlockID]cacheBlockIndex + +type logicalBlockID struct { + filenum base.DiskFileNum + cacheBlockIdx cacheBlockIndex +} + +type lockState int64 + +const ( + unlocked lockState = 0 + // >0 lockState tracks the number of distinct readers of some cache block / logical block + // which is in the secondary cache. It is used to ensure that a cache block is not evicted + // and overwritten, while there are active readers. + readLockTakenInc = 1 + // -1 lockState indicates that some cache block is currently being populated with data from + // blob storage. It is used to ensure that a cache block is not read or evicted again, while + // it is being populated. + writeLockTaken = -1 +) + +func (s *shard) init( + cache *Cache, + fs vfs.FS, + fsDir string, + shardIdx int, + sizeInBlocks int64, + blockSize int, + shardingBlockSize int64, +) error { + *s = shard{ + cache: cache, + sizeInBlocks: sizeInBlocks, + } + if blockSize < 1024 || shardingBlockSize%int64(blockSize) != 0 { + return errors.Newf("invalid block size %d (must divide %d)", blockSize, shardingBlockSize) + } + s.bm = makeBlockMath(blockSize) + s.shardingBlockSize = shardingBlockSize + file, err := fs.OpenReadWrite(fs.PathJoin(fsDir, fmt.Sprintf("SHARED-CACHE-%03d", shardIdx))) + if err != nil { + return err + } + // TODO(radu): truncate file if necessary (especially important if we restart + // with more shards). + if err := file.Preallocate(0, int64(blockSize)*sizeInBlocks); err != nil { + return err + } + s.file = file + + // TODO(josh): Right now, the secondary cache is not persistent. All existing + // cache contents will be over-written, since all metadata is only stored in + // memory. + s.mu.where = make(whereMap) + s.mu.blocks = make([]cacheBlockState, sizeInBlocks) + s.mu.lruHead = invalidBlockIndex + s.mu.freeHead = invalidBlockIndex + for i := range s.mu.blocks { + s.freePush(cacheBlockIndex(i)) + } + + return nil +} + +func (s *shard) close() error { + defer func() { + s.file = nil + }() + return s.file.Close() +} + +// freePush pushes a block to the front of the free list. +func (s *shard) freePush(index cacheBlockIndex) { + s.mu.blocks[index].next = s.mu.freeHead + s.mu.freeHead = index +} + +// freePop removes the block from the front of the free list. Must not be called +// if the list is empty (i.e. freeHead = invalidBlockIndex). +func (s *shard) freePop() cacheBlockIndex { + index := s.mu.freeHead + s.mu.freeHead = s.mu.blocks[index].next + return index +} + +// lruInsertFront inserts a block at the front of the LRU list. +func (s *shard) lruInsertFront(index cacheBlockIndex) { + b := &s.mu.blocks[index] + if s.mu.lruHead == invalidBlockIndex { + b.next = index + b.prev = index + } else { + b.next = s.mu.lruHead + h := &s.mu.blocks[s.mu.lruHead] + b.prev = h.prev + s.mu.blocks[h.prev].next = index + h.prev = index + } + s.mu.lruHead = index +} + +func (s *shard) lruNext(index cacheBlockIndex) cacheBlockIndex { + return s.mu.blocks[index].next +} + +func (s *shard) lruPrev(index cacheBlockIndex) cacheBlockIndex { + return s.mu.blocks[index].prev +} + +// lruUnlink removes a block from the LRU list. +func (s *shard) lruUnlink(index cacheBlockIndex) { + b := &s.mu.blocks[index] + if b.next == index { + s.mu.lruHead = invalidBlockIndex + } else { + s.mu.blocks[b.prev].next = b.next + s.mu.blocks[b.next].prev = b.prev + if s.mu.lruHead == index { + s.mu.lruHead = b.next + } + } + b.next, b.prev = invalidBlockIndex, invalidBlockIndex +} + +// get attempts to read the requested data from the shard. The data must not +// cross a shard boundary. +// +// If all data is available, returns n = len(p). +// +// If data is partially available, a prefix of the data is read; returns n < len(p) +// and no error. If no prefix is available, returns n = 0 and no error. +// +// TODO(josh): Today, if there are two cache blocks needed to satisfy a read, and the +// first block is not in the cache and the second one is, we will read both from +// blob storage. We should fix this. This is not an unlikely scenario if we are doing +// a reverse scan, since those iterate over sstable blocks in reverse order and due to +// cache block aligned reads will have read the suffix of the sstable block that will +// be needed next. +func (s *shard) get(fileNum base.DiskFileNum, p []byte, ofs int64) (n int, _ error) { + if invariants.Enabled { + if ofs/s.shardingBlockSize != (ofs+int64(len(p))-1)/s.shardingBlockSize { + panic(fmt.Sprintf("get crosses shard boundary: %v %v", ofs, len(p))) + } + s.assertShardStateIsConsistent() + } + + // The data extent might cross cache block boundaries, hence the loop. In the hot + // path, max two iterations of this loop will be executed, since reads are sized + // in units of sstable block size. + var multiBlock bool + for { + k := logicalBlockID{ + filenum: fileNum, + cacheBlockIdx: s.bm.Block(ofs + int64(n)), + } + s.mu.Lock() + cacheBlockIdx, ok := s.mu.where[k] + // TODO(josh): Multiple reads within the same few milliseconds (anything that is smaller + // than blob storage read latency) that miss on the same logical block ID will not necessarily + // be rare. We may want to do only one read, with the later readers blocking on the first read + // completing. This could be implemented either here or in the primary block cache. See + // https://github.com/cockroachdb/pebble/pull/2586 for additional discussion. + if !ok { + s.mu.Unlock() + return n, nil + } + if s.mu.blocks[cacheBlockIdx].lock == writeLockTaken { + // In practice, if we have two reads of the same SST block in close succession, we + // would expect the second to hit in the in-memory block cache. So it's not worth + // optimizing this case here. + s.mu.Unlock() + return n, nil + } + s.mu.blocks[cacheBlockIdx].lock += readLockTakenInc + // Move to front of the LRU list. + s.lruUnlink(cacheBlockIdx) + s.lruInsertFront(cacheBlockIdx) + s.mu.Unlock() + + readAt := s.bm.BlockOffset(cacheBlockIdx) + readSize := s.bm.BlockSize() + if n == 0 { // if first read + rem := s.bm.Remainder(ofs) + readAt += rem + readSize -= int(rem) + } + + if len(p[n:]) <= readSize { + start := time.Now() + numRead, err := s.file.ReadAt(p[n:], readAt) + s.cache.metrics.diskReadLatency.Observe(float64(time.Since(start))) + s.dropReadLock(cacheBlockIdx) + return n + numRead, err + } + start := time.Now() + numRead, err := s.file.ReadAt(p[n:n+readSize], readAt) + s.cache.metrics.diskReadLatency.Observe(float64(time.Since(start))) + s.dropReadLock(cacheBlockIdx) + if err != nil { + return 0, err + } + + // Note that numRead == readSize, since we checked for an error above. + n += numRead + + if !multiBlock { + s.cache.metrics.multiBlockReads.Add(1) + multiBlock = true + } + } +} + +// set attempts to write the requested data to the shard. The data must not +// cross a shard boundary, and both ofs & len(p) must be multiples of the +// block size. +// +// If all of p is not written to the shard, set returns a non-nil error. +func (s *shard) set(fileNum base.DiskFileNum, p []byte, ofs int64) error { + if invariants.Enabled { + if ofs/s.shardingBlockSize != (ofs+int64(len(p))-1)/s.shardingBlockSize { + panic(fmt.Sprintf("set crosses shard boundary: %v %v", ofs, len(p))) + } + if s.bm.Remainder(ofs) != 0 || s.bm.Remainder(int64(len(p))) != 0 { + panic(fmt.Sprintf("set with ofs & len not multiples of block size: %v %v", ofs, len(p))) + } + s.assertShardStateIsConsistent() + } + + // The data extent might cross cache block boundaries, hence the loop. In the hot + // path, max two iterations of this loop will be executed, since reads are sized + // in units of sstable block size. + n := 0 + for { + if n == len(p) { + return nil + } + if invariants.Enabled { + if n > len(p) { + panic(fmt.Sprintf("set with n greater than len(p): %v %v", n, len(p))) + } + } + + // If the logical block is already in the cache, we should skip doing a set. + k := logicalBlockID{ + filenum: fileNum, + cacheBlockIdx: s.bm.Block(ofs + int64(n)), + } + s.mu.Lock() + if _, ok := s.mu.where[k]; ok { + s.mu.Unlock() + n += s.bm.BlockSize() + continue + } + + var cacheBlockIdx cacheBlockIndex + if s.mu.freeHead == invalidBlockIndex { + if invariants.Enabled && s.mu.lruHead == invalidBlockIndex { + panic("both LRU and free lists empty") + } + + // Find the last element in the LRU list which is not locked. + for idx := s.lruPrev(s.mu.lruHead); ; idx = s.lruPrev(idx) { + if lock := s.mu.blocks[idx].lock; lock == unlocked { + cacheBlockIdx = idx + break + } + if idx == s.mu.lruHead { + // No unlocked block to evict. + // + // TODO(josh): We may want to block until a block frees up, instead of returning + // an error here. But I think we can do that later on, e.g. after running some production + // experiments. + s.mu.Unlock() + return errors.New("no block to evict so skipping write to cache") + } + } + s.cache.metrics.evictions.Add(1) + s.lruUnlink(cacheBlockIdx) + delete(s.mu.where, s.mu.blocks[cacheBlockIdx].logical) + } else { + s.cache.metrics.count.Add(1) + cacheBlockIdx = s.freePop() + } + + s.lruInsertFront(cacheBlockIdx) + s.mu.where[k] = cacheBlockIdx + s.mu.blocks[cacheBlockIdx].logical = k + s.mu.blocks[cacheBlockIdx].lock = writeLockTaken + s.mu.Unlock() + + writeAt := s.bm.BlockOffset(cacheBlockIdx) + + writeSize := s.bm.BlockSize() + if len(p[n:]) <= writeSize { + writeSize = len(p[n:]) + } + + start := time.Now() + _, err := s.file.WriteAt(p[n:n+writeSize], writeAt) + s.cache.metrics.diskWriteLatency.Observe(float64(time.Since(start))) + if err != nil { + // Free the block. + s.mu.Lock() + defer s.mu.Unlock() + + delete(s.mu.where, k) + s.lruUnlink(cacheBlockIdx) + s.freePush(cacheBlockIdx) + return err + } + s.dropWriteLock(cacheBlockIdx) + n += writeSize + } +} + +// Doesn't inline currently. This might be okay, but something to keep in mind. +func (s *shard) dropReadLock(cacheBlockInd cacheBlockIndex) { + s.mu.Lock() + s.mu.blocks[cacheBlockInd].lock -= readLockTakenInc + if invariants.Enabled && s.mu.blocks[cacheBlockInd].lock < 0 { + panic(fmt.Sprintf("unexpected lock state %v in dropReadLock", s.mu.blocks[cacheBlockInd].lock)) + } + s.mu.Unlock() +} + +// Doesn't inline currently. This might be okay, but something to keep in mind. +func (s *shard) dropWriteLock(cacheBlockInd cacheBlockIndex) { + s.mu.Lock() + if invariants.Enabled && s.mu.blocks[cacheBlockInd].lock != writeLockTaken { + panic(fmt.Sprintf("unexpected lock state %v in dropWriteLock", s.mu.blocks[cacheBlockInd].lock)) + } + s.mu.blocks[cacheBlockInd].lock = unlocked + s.mu.Unlock() +} + +func (s *shard) assertShardStateIsConsistent() { + s.mu.Lock() + defer s.mu.Unlock() + + lruLen := 0 + if s.mu.lruHead != invalidBlockIndex { + for b := s.mu.lruHead; ; { + lruLen++ + if idx, ok := s.mu.where[s.mu.blocks[b].logical]; !ok || idx != b { + panic("block in LRU list with no entry in where map") + } + b = s.lruNext(b) + if b == s.mu.lruHead { + break + } + } + } + if lruLen != len(s.mu.where) { + panic(fmt.Sprintf("lru list len is %d but where map has %d entries", lruLen, len(s.mu.where))) + } + freeLen := 0 + for n := s.mu.freeHead; n != invalidBlockIndex; n = s.mu.blocks[n].next { + freeLen++ + } + + if lruLen+freeLen != int(s.sizeInBlocks) { + panic(fmt.Sprintf("%d lru blocks and %d free blocks don't add up to %d", lruLen, freeLen, s.sizeInBlocks)) + } + for i := range s.mu.blocks { + if state := s.mu.blocks[i].lock; state < writeLockTaken { + panic(fmt.Sprintf("lock state %v is not allowed", state)) + } + } +} + +// cacheBlockIndex is the index of a blockSize-aligned cache block. +type cacheBlockIndex int64 + +// invalidBlockIndex is used for the head of a list when the list is empty. +const invalidBlockIndex cacheBlockIndex = -1 + +// blockMath is a helper type for performing conversions between offsets and +// block indexes. +type blockMath struct { + blockSizeBits int8 +} + +func makeBlockMath(blockSize int) blockMath { + bm := blockMath{ + blockSizeBits: int8(bits.Len64(uint64(blockSize)) - 1), + } + if blockSize != (1 << bm.blockSizeBits) { + panic(fmt.Sprintf("blockSize %d is not a power of 2", blockSize)) + } + return bm +} + +func (bm blockMath) mask() int64 { + return (1 << bm.blockSizeBits) - 1 +} + +// BlockSize returns the block size. +func (bm blockMath) BlockSize() int { + return 1 << bm.blockSizeBits +} + +// Block returns the block index containing the given offset. +func (bm blockMath) Block(offset int64) cacheBlockIndex { + return cacheBlockIndex(offset >> bm.blockSizeBits) +} + +// Remainder returns the offset relative to the start of the cache block. +func (bm blockMath) Remainder(offset int64) int64 { + return offset & bm.mask() +} + +// BlockOffset returns the object offset where the given block starts. +func (bm blockMath) BlockOffset(block cacheBlockIndex) int64 { + return int64(block) << bm.blockSizeBits +} + +// RoundUp rounds up the given value to the closest multiple of block size. +func (bm blockMath) RoundUp(x int64) int64 { + return (x + bm.mask()) & ^(bm.mask()) +} + +type writeWorkers struct { + doneCh chan struct{} + doneWaitGroup sync.WaitGroup + + numWorkers int + tasksCh chan writeTask +} + +type writeTask struct { + fileNum base.DiskFileNum + p []byte + offset int64 +} + +// Start starts the worker goroutines. +func (w *writeWorkers) Start(c *Cache, numWorkers int) { + doneCh := make(chan struct{}) + tasksCh := make(chan writeTask, numWorkers*writeTasksPerWorker) + + w.numWorkers = numWorkers + w.doneCh = doneCh + w.tasksCh = tasksCh + w.doneWaitGroup.Add(numWorkers) + for i := 0; i < numWorkers; i++ { + go func() { + defer w.doneWaitGroup.Done() + for { + select { + case <-doneCh: + return + case task, ok := <-tasksCh: + if !ok { + // The tasks channel was closed; this is used in testing code to + // ensure all writes are completed. + return + } + // TODO(radu): set() can perform multiple writes; perhaps each one + // should be its own task. + start := time.Now() + err := c.set(task.fileNum, task.p, task.offset) + c.metrics.putLatency.Observe(float64(time.Since(start))) + if err != nil { + c.metrics.writeBackFailures.Add(1) + // TODO(radu): throttle logs. + c.logger.Infof("writing back to cache after miss failed: %v", err) + } + } + } + }() + } +} + +// Stop waits for any in-progress writes to complete and stops the worker +// goroutines and waits for any in-pro. Any queued writes not yet started are +// discarded. +func (w *writeWorkers) Stop() { + close(w.doneCh) + w.doneCh = nil + w.tasksCh = nil + w.doneWaitGroup.Wait() +} + +// QueueWrite adds a write task to the queue. Can block if the queue is full. +func (w *writeWorkers) QueueWrite(fileNum base.DiskFileNum, p []byte, offset int64) { + w.tasksCh <- writeTask{ + fileNum: fileNum, + p: p, + offset: offset, + } +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/vfs.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs.go similarity index 60% rename from vendor/github.com/cockroachdb/pebble/objstorage/vfs.go rename to vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs.go index b997a18..5b20251 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/vfs.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs.go @@ -2,63 +2,63 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -package objstorage +package objstorageprovider import ( + "context" + "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/vfs" ) -func (p *Provider) vfsPath(fileType base.FileType, fileNum base.FileNum) string { +func (p *provider) vfsPath(fileType base.FileType, fileNum base.DiskFileNum) string { return base.MakeFilepath(p.st.FS, p.st.FSDirName, fileType, fileNum) } -func (p *Provider) vfsOpenForReading( - fileType base.FileType, fileNum base.FileNum, mustExist bool, -) (Readable, error) { +func (p *provider) vfsOpenForReading( + ctx context.Context, + fileType base.FileType, + fileNum base.DiskFileNum, + opts objstorage.OpenOptions, +) (objstorage.Readable, error) { filename := p.vfsPath(fileType, fileNum) file, err := p.st.FS.Open(filename, vfs.RandomReadsOption) if err != nil { - if mustExist { + if opts.MustExist { base.MustExist(p.st.FS, filename, p.st.Logger, err) } return nil, err } - // TODO(radu): we use the existence of the file descriptor as an indication - // that the File might support Prefetch and SequentialReadsOption. We should - // replace this with a cleaner way to obtain the capabilities of the FS / File. - if fd := file.Fd(); fd != vfs.InvalidFd { - return newFileReadable(file, p.st.FS, filename) - } - return newGenericFileReadable(file) + return newFileReadable(file, p.st.FS, filename) } -func (p *Provider) vfsCreate( - fileType base.FileType, fileNum base.FileNum, -) (Writable, ObjectMetadata, error) { +func (p *provider) vfsCreate( + _ context.Context, fileType base.FileType, fileNum base.DiskFileNum, +) (objstorage.Writable, objstorage.ObjectMetadata, error) { filename := p.vfsPath(fileType, fileNum) file, err := p.st.FS.Create(filename) if err != nil { - return nil, ObjectMetadata{}, err + return nil, objstorage.ObjectMetadata{}, err } file = vfs.NewSyncingFile(file, vfs.SyncingFileOptions{ NoSyncOnClose: p.st.NoSyncOnClose, BytesPerSync: p.st.BytesPerSync, }) - meta := ObjectMetadata{ - FileNum: fileNum, - FileType: fileType, + meta := objstorage.ObjectMetadata{ + DiskFileNum: fileNum, + FileType: fileType, } return newFileBufferedWritable(file), meta, nil } -func (p *Provider) vfsRemove(fileType base.FileType, fileNum base.FileNum) error { +func (p *provider) vfsRemove(fileType base.FileType, fileNum base.DiskFileNum) error { return p.st.FSCleaner.Clean(p.st.FS, fileType, p.vfsPath(fileType, fileNum)) } // vfsInit finds any local FS objects. -func (p *Provider) vfsInit() error { +func (p *provider) vfsInit() error { listing := p.st.FSDirInitialListing if listing == nil { var err error @@ -71,17 +71,17 @@ func (p *Provider) vfsInit() error { for _, filename := range listing { fileType, fileNum, ok := base.ParseFilename(p.st.FS, filename) if ok && fileType == base.FileTypeTable { - o := ObjectMetadata{ - FileType: fileType, - FileNum: fileNum, + o := objstorage.ObjectMetadata{ + FileType: fileType, + DiskFileNum: fileNum, } - p.mu.knownObjects[o.FileNum] = o + p.mu.knownObjects[o.DiskFileNum] = o } } return nil } -func (p *Provider) vfsSync() error { +func (p *provider) vfsSync() error { p.mu.Lock() shouldSync := p.mu.localObjectsChanged p.mu.localObjectsChanged = false @@ -99,7 +99,7 @@ func (p *Provider) vfsSync() error { return nil } -func (p *Provider) vfsSize(fileType base.FileType, fileNum base.FileNum) (int64, error) { +func (p *provider) vfsSize(fileType base.FileType, fileNum base.DiskFileNum) (int64, error) { filename := p.vfsPath(fileType, fileNum) stat, err := p.st.FS.Stat(filename) if err != nil { diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/vfs_readable.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_readable.go similarity index 62% rename from vendor/github.com/cockroachdb/pebble/objstorage/vfs_readable.go rename to vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_readable.go index 22749c5..9362464 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/vfs_readable.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_readable.go @@ -2,19 +2,25 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -package objstorage +package objstorageprovider import ( + "context" "fmt" "os" "sync" "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/vfs" ) -// fileReadable implements objstorage.Readable on top of a vfs.File that -// supports Prefetch and/or on an FS that supports SequentialReadsOption. +const fileMaxReadaheadSize = 256 * 1024 /* 256KB */ + +// fileReadable implements objstorage.Readable on top of a vfs.File. +// +// The implementation might use Prealloc and might reopen the file with +// SequentialReadsOption. type fileReadable struct { file vfs.File size int64 @@ -25,7 +31,7 @@ type fileReadable struct { fs vfs.FS } -var _ Readable = (*fileReadable)(nil) +var _ objstorage.Readable = (*fileReadable)(nil) func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable, error) { info, err := file.Stat() @@ -48,8 +54,12 @@ func newFileReadable(file vfs.File, fs vfs.FS, filename string) (*fileReadable, } // ReadAt is part of the objstorage.Readable interface. -func (r *fileReadable) ReadAt(p []byte, off int64) (n int, err error) { - return r.file.ReadAt(p, off) +func (r *fileReadable) ReadAt(_ context.Context, p []byte, off int64) error { + n, err := r.file.ReadAt(p, off) + if invariants.Enabled && err == nil && n != len(p) { + panic("short read") + } + return err } // Close is part of the objstorage.Readable interface. @@ -64,10 +74,10 @@ func (r *fileReadable) Size() int64 { } // NewReadHandle is part of the objstorage.Readable interface. -func (r *fileReadable) NewReadHandle() ReadHandle { +func (r *fileReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle { rh := readHandlePool.Get().(*vfsReadHandle) rh.r = r - rh.rs = makeReadaheadState() + rh.rs = makeReadaheadState(fileMaxReadaheadSize) return rh } @@ -82,7 +92,7 @@ type vfsReadHandle struct { sequentialFile vfs.File } -var _ ReadHandle = (*vfsReadHandle)(nil) +var _ objstorage.ReadHandle = (*vfsReadHandle)(nil) var readHandlePool = sync.Pool{ New: func() interface{} { @@ -110,25 +120,36 @@ func (rh *vfsReadHandle) Close() error { } // ReadAt is part of the objstorage.ReadHandle interface. -func (rh *vfsReadHandle) ReadAt(p []byte, offset int64) (n int, err error) { +func (rh *vfsReadHandle) ReadAt(_ context.Context, p []byte, offset int64) error { + var n int + var err error if rh.sequentialFile != nil { // Use OS-level read-ahead. - return rh.sequentialFile.ReadAt(p, offset) - } - if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 { - if readaheadSize >= maxReadaheadSize { - // We've reached the maximum readahead size. Beyond this point, rely on - // OS-level readahead. - rh.MaxReadahead() - } else { - _ = rh.r.file.Prefetch(offset, readaheadSize) + n, err = rh.sequentialFile.ReadAt(p, offset) + } else { + if readaheadSize := rh.rs.maybeReadahead(offset, int64(len(p))); readaheadSize > 0 { + if readaheadSize >= fileMaxReadaheadSize { + // We've reached the maximum readahead size. Beyond this point, rely on + // OS-level readahead. + rh.switchToOSReadahead() + } else { + _ = rh.r.file.Prefetch(offset, readaheadSize) + } } + n, err = rh.r.file.ReadAt(p, offset) } - return rh.r.file.ReadAt(p, offset) + if invariants.Enabled && err == nil && n != len(p) { + panic("short read") + } + return err } -// MaxReadahead is part of the objstorage.ReadHandle interface. -func (rh *vfsReadHandle) MaxReadahead() { +// SetupForCompaction is part of the objstorage.ReadHandle interface. +func (rh *vfsReadHandle) SetupForCompaction() { + rh.switchToOSReadahead() +} + +func (rh *vfsReadHandle) switchToOSReadahead() { if rh.sequentialFile != nil { return } @@ -142,7 +163,7 @@ func (rh *vfsReadHandle) MaxReadahead() { } // RecordCacheHit is part of the objstorage.ReadHandle interface. -func (rh *vfsReadHandle) RecordCacheHit(offset, size int64) { +func (rh *vfsReadHandle) RecordCacheHit(_ context.Context, offset, size int64) { if rh.sequentialFile != nil { // Using OS-level readahead, so do nothing. return @@ -150,60 +171,9 @@ func (rh *vfsReadHandle) RecordCacheHit(offset, size int64) { rh.rs.recordCacheHit(offset, size) } -// genericFileReadable implements objstorage.Readable on top of any vfs.File. -// This implementation does not support read-ahead. -type genericFileReadable struct { - file vfs.File - size int64 - - rh NoopReadHandle -} - -var _ Readable = (*genericFileReadable)(nil) - -func newGenericFileReadable(file vfs.File) (*genericFileReadable, error) { - info, err := file.Stat() - if err != nil { - return nil, err - } - r := &genericFileReadable{ - file: file, - size: info.Size(), - rh: MakeNoopReadHandle(file), - } - invariants.SetFinalizer(r, func(obj interface{}) { - if obj.(*genericFileReadable).file != nil { - fmt.Fprintf(os.Stderr, "Readable was not closed") - os.Exit(1) - } - }) - return r, nil -} - -// ReadAt is part of the objstorage.Readable interface. -func (r *genericFileReadable) ReadAt(p []byte, off int64) (n int, err error) { - return r.file.ReadAt(p, off) -} - -// Close is part of the objstorage.Readable interface. -func (r *genericFileReadable) Close() error { - defer func() { r.file = nil }() - return r.file.Close() -} - -// Size is part of the objstorage.Readable interface. -func (r *genericFileReadable) Size() int64 { - return r.size -} - -// NewReadHandle is part of the objstorage.Readable interface. -func (r *genericFileReadable) NewReadHandle() ReadHandle { - return &r.rh -} - // TestingCheckMaxReadahead returns true if the ReadHandle has switched to // OS-level read-ahead. -func TestingCheckMaxReadahead(rh ReadHandle) bool { +func TestingCheckMaxReadahead(rh objstorage.ReadHandle) bool { switch rh := rh.(type) { case *vfsReadHandle: return rh.sequentialFile != nil @@ -234,11 +204,13 @@ func (rh *PreallocatedReadHandle) Close() error { // but uses the existing storage of a PreallocatedReadHandle when possible // (currently this happens if we are reading from a local file). // The returned handle still needs to be closed. -func UsePreallocatedReadHandle(readable Readable, rh *PreallocatedReadHandle) ReadHandle { +func UsePreallocatedReadHandle( + ctx context.Context, readable objstorage.Readable, rh *PreallocatedReadHandle, +) objstorage.ReadHandle { if r, ok := readable.(*fileReadable); ok { // See fileReadable.NewReadHandle. rh.vfsReadHandle = vfsReadHandle{r: r} return rh } - return readable.NewReadHandle() + return readable.NewReadHandle(ctx) } diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/vfs_writable.go b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_writable.go similarity index 87% rename from vendor/github.com/cockroachdb/pebble/objstorage/vfs_writable.go rename to vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_writable.go index 376d42c..c9b207c 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/vfs_writable.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/objstorageprovider/vfs_writable.go @@ -2,16 +2,17 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -package objstorage +package objstorageprovider import ( "bufio" + "github.com/cockroachdb/pebble/objstorage" "github.com/cockroachdb/pebble/vfs" ) // NewFileWritable returns a Writable that uses a file as underlying storage. -func NewFileWritable(file vfs.File) Writable { +func NewFileWritable(file vfs.File) objstorage.Writable { return newFileBufferedWritable(file) } @@ -20,7 +21,7 @@ type fileBufferedWritable struct { bw *bufio.Writer } -var _ Writable = (*fileBufferedWritable)(nil) +var _ objstorage.Writable = (*fileBufferedWritable)(nil) func newFileBufferedWritable(file vfs.File) *fileBufferedWritable { return &fileBufferedWritable{ diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/provider.go b/vendor/github.com/cockroachdb/pebble/objstorage/provider.go deleted file mode 100644 index 145abe7..0000000 --- a/vendor/github.com/cockroachdb/pebble/objstorage/provider.go +++ /dev/null @@ -1,446 +0,0 @@ -// Copyright 2023 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 objstorage - -import ( - "io" - "os" - "sort" - "sync" - - "github.com/cockroachdb/errors" - "github.com/cockroachdb/errors/oserror" - "github.com/cockroachdb/pebble/internal/base" - "github.com/cockroachdb/pebble/objstorage/shared" - "github.com/cockroachdb/pebble/objstorage/sharedobjcat" - "github.com/cockroachdb/pebble/vfs" -) - -// Provider is a singleton object used to access and manage objects. -// -// An object is conceptually like a large immutable file. The main use of -// objects is for storing sstables; in the future it could also be used for blob -// storage. -// -// The Provider can only manage objects that it knows about - either objects -// created by the provider, or existing objects the Provider was informed about -// via AddObjects. -// -// Objects are currently backed by a vfs.File. -type Provider struct { - st Settings - - fsDir vfs.File - - shared sharedSubsystem - - mu struct { - sync.RWMutex - - shared struct { - // catalogBatch accumulates shared object creations and deletions until - // Sync is called. - catalogBatch sharedobjcat.Batch - } - - // localObjectsChanged is set if non-shared objects were created or deleted - // but Sync was not yet called. - localObjectsChanged bool - - // knownObjects maintains information about objects that are known to the provider. - // It is initialized with the list of files in the manifest when we open a DB. - knownObjects map[base.FileNum]ObjectMetadata - } -} - -// Readable is the handle for an object that is open for reading. -type Readable interface { - io.ReaderAt - io.Closer - - // Size returns the size of the object. - Size() int64 - - // NewReadHandle creates a read handle for ReadAt requests that are related - // and can benefit from optimizations like read-ahead. - // - // The ReadHandle must be closed before the Readable is closed. - // - // Multiple separate ReadHandles can be used. - NewReadHandle() ReadHandle -} - -// ReadHandle is used to perform reads that are related and might benefit from -// optimizations like read-ahead. -type ReadHandle interface { - io.ReaderAt - io.Closer - - // MaxReadahead configures the implementation to expect large sequential - // reads. Used to skip any initial read-ahead ramp-up. - MaxReadahead() - - // RecordCacheHit informs the implementation that we were able to retrieve a - // block from cache. - RecordCacheHit(offset, size int64) -} - -// Writable is the handle for an object that is open for writing. -// Either Finish or Abort must be called. -type Writable interface { - // Write writes len(p) bytes from p to the underlying object. The data is not - // guaranteed to be durable until Finish is called. - // - // Note that Write *is* allowed to modify the slice passed in, whether - // temporarily or permanently. Callers of Write need to take this into - // account. - Write(p []byte) error - - // Finish completes the object and makes the data durable. - // No further calls are allowed after calling Finish. - Finish() error - - // Abort gives up on finishing the object. There is no guarantee about whether - // the object exists after calling Abort. - // No further calls are allowed after calling Abort. - Abort() -} - -// Settings that must be specified when creating the Provider. -type Settings struct { - Logger base.Logger - - // Local filesystem configuration. - FS vfs.FS - FSDirName string - - // FSDirInitialListing is a listing of FSDirName at the time of calling Open. - // - // This is an optional optimization to avoid double listing on Open when the - // higher layer already has a listing. When nil, we obtain the listing on - // Open. - FSDirInitialListing []string - - // Cleaner cleans obsolete files from the local filesystem. - // - // The default cleaner uses the DeleteCleaner. - FSCleaner base.Cleaner - - // NoSyncOnClose decides whether the implementation will enforce a - // close-time synchronization (e.g., fdatasync() or sync_file_range()) - // on files it writes to. Setting this to true removes the guarantee for a - // sync on close. Some implementations can still issue a non-blocking sync. - NoSyncOnClose bool - - // BytesPerSync enables periodic syncing of files in order to smooth out - // writes to disk. This option does not provide any persistence guarantee, but - // is used to avoid latency spikes if the OS automatically decides to write - // out a large chunk of dirty filesystem buffers. - BytesPerSync int - - // Fields here are set only if the provider is to support shared objects - // (experimental). - Shared struct { - Storage shared.Storage - } -} - -// DefaultSettings initializes default settings (with no shared storage), -// suitable for tests and tools. -func DefaultSettings(fs vfs.FS, dirName string) Settings { - return Settings{ - Logger: base.DefaultLogger, - FS: fs, - FSDirName: dirName, - FSCleaner: base.DeleteCleaner{}, - NoSyncOnClose: false, - BytesPerSync: 512 * 1024, // 512KB - } -} - -// ObjectMetadata contains the metadata required to be able to access an object. -type ObjectMetadata struct { - FileNum base.FileNum - FileType base.FileType - - // The fields below are only set if the object is on shared storage. - Shared struct { - // CreatorID identifies the DB instance that originally created the object. - CreatorID CreatorID - // CreatorFileNum is the identifier for the object within the context of the - // DB instance that originally created the object. - CreatorFileNum base.FileNum - } -} - -// CreatorID identifies the DB instance that originally created a shared object. -// This ID is incorporated in backing object names. -// Must be non-zero. -type CreatorID = sharedobjcat.CreatorID - -// IsShared returns true if the object is on shared storage. -func (meta *ObjectMetadata) IsShared() bool { - return meta.Shared.CreatorID.IsSet() -} - -// Open creates the Provider. -func Open(settings Settings) (p *Provider, _ error) { - fsDir, err := settings.FS.OpenDir(settings.FSDirName) - if err != nil { - return nil, err - } - - defer func() { - if p == nil { - fsDir.Close() - } - }() - - p = &Provider{ - st: settings, - fsDir: fsDir, - } - p.mu.knownObjects = make(map[base.FileNum]ObjectMetadata) - - // Add local FS objects. - if err := p.vfsInit(); err != nil { - return nil, err - } - - // Initialize shared subsystem (if configured) and add shared objects. - if err := p.sharedInit(); err != nil { - return nil, err - } - - return p, nil -} - -// Close the provider. -func (p *Provider) Close() error { - var err error - if p.fsDir != nil { - err = p.fsDir.Close() - p.fsDir = nil - } - return err -} - -// OpenForReading opens an existing object. -func (p *Provider) OpenForReading(fileType base.FileType, fileNum base.FileNum) (Readable, error) { - meta, err := p.Lookup(fileType, fileNum) - if err != nil { - return nil, err - } - - if !meta.IsShared() { - return p.vfsOpenForReading(fileType, fileNum, false /* mustExist */) - } - return p.sharedOpenForReading(meta) -} - -// OpenForReadingMustExist is a variant of OpenForReading which causes a fatal -// error if the file does not exist. The fatal error message contains -// information helpful for debugging. -func (p *Provider) OpenForReadingMustExist( - fileType base.FileType, fileNum base.FileNum, -) (Readable, error) { - meta, err := p.Lookup(fileType, fileNum) - if err != nil { - p.st.Logger.Fatalf("%v", err) - return nil, err - } - - if !meta.IsShared() { - return p.vfsOpenForReading(fileType, fileNum, true /* mustExist */) - } - - // TODO(radu): implement "must exist" behavior. - return p.sharedOpenForReading(meta) -} - -// CreateOptions contains optional arguments for Create. -type CreateOptions struct { - // PreferSharedStorage causes the object to be created on shared storage if - // the provider has shared storage configured. - PreferSharedStorage bool -} - -// Create creates a new object and opens it for writing. -// -// The object is not guaranteed to be durable (accessible in case of crashes) -// until Sync is called. -func (p *Provider) Create( - fileType base.FileType, fileNum base.FileNum, opts CreateOptions, -) (w Writable, meta ObjectMetadata, err error) { - if opts.PreferSharedStorage && p.st.Shared.Storage != nil { - w, meta, err = p.sharedCreate(fileType, fileNum) - } else { - w, meta, err = p.vfsCreate(fileType, fileNum) - } - if err != nil { - err = errors.Wrapf(err, "creating object %s", errors.Safe(fileNum)) - return nil, ObjectMetadata{}, err - } - p.addMetadata(meta) - return w, meta, nil -} - -// Remove removes an object. -// -// The object is not guaranteed to be durably removed until Sync is called. -func (p *Provider) Remove(fileType base.FileType, fileNum base.FileNum) error { - meta, err := p.Lookup(fileType, fileNum) - if err != nil { - return err - } - - if !meta.IsShared() { - err = p.vfsRemove(fileType, fileNum) - } - // TODO(radu): implement shared object removal (i.e. deref). - - if err != nil && !IsNotExistError(err) { - // We want to be able to retry a Remove, so we keep the object in our list. - // TODO(radu): we should mark the object as "zombie" and not allow any other - // operations. - return errors.Wrapf(err, "removing object %s", errors.Safe(fileNum)) - } - p.removeMetadata(fileNum) - return err -} - -// IsNotExistError indicates whether the error is known to report that a file or -// directory does not exist. -func IsNotExistError(err error) bool { - return oserror.IsNotExist(err) -} - -// Sync flushes the metadata from creation or removal of objects since the last Sync. -func (p *Provider) Sync() error { - if err := p.vfsSync(); err != nil { - return err - } - if err := p.sharedSync(); err != nil { - return err - } - return nil -} - -// LinkOrCopyFromLocal creates a new object that is either a copy of a given -// local file or a hard link (if the new object is created on the same FS, and -// if the FS supports it). -// -// The object is not guaranteed to be durable (accessible in case of crashes) -// until Sync is called. -func (p *Provider) LinkOrCopyFromLocal( - srcFS vfs.FS, srcFilePath string, dstFileType base.FileType, dstFileNum base.FileNum, -) (ObjectMetadata, error) { - if srcFS == p.st.FS { - // Wrap the normal filesystem with one which wraps newly created files with - // vfs.NewSyncingFile. - fs := vfs.NewSyncingFS(p.st.FS, vfs.SyncingFileOptions{ - NoSyncOnClose: p.st.NoSyncOnClose, - BytesPerSync: p.st.BytesPerSync, - }) - dstPath := p.vfsPath(dstFileType, dstFileNum) - if err := vfs.LinkOrCopy(fs, srcFilePath, dstPath); err != nil { - return ObjectMetadata{}, err - } - - meta := ObjectMetadata{ - FileNum: dstFileNum, - FileType: dstFileType, - } - p.addMetadata(meta) - return meta, nil - } - // TODO(radu): for the copy case, we should use `p.Create` and do the copy ourselves. - panic("unimplemented") -} - -// Lookup returns the metadata of an object that is already known to the Provider. -// Does not perform any I/O. -func (p *Provider) Lookup(fileType base.FileType, fileNum base.FileNum) (ObjectMetadata, error) { - p.mu.RLock() - defer p.mu.RUnlock() - meta, ok := p.mu.knownObjects[fileNum] - if !ok { - return ObjectMetadata{}, errors.Wrapf( - os.ErrNotExist, - "file %s (type %d) unknown to the objstorage provider", - errors.Safe(fileNum), errors.Safe(fileType), - ) - } - if meta.FileType != fileType { - return ObjectMetadata{}, errors.AssertionFailedf( - "file %s type mismatch (known type %d, expected type %d)", - errors.Safe(fileNum), errors.Safe(meta.FileType), errors.Safe(fileType), - ) - } - return meta, nil -} - -// Path returns an internal, implementation-dependent path for the object. It is -// meant to be used for informational purposes (like logging). -func (p *Provider) Path(meta ObjectMetadata) string { - if !meta.IsShared() { - return p.vfsPath(meta.FileType, meta.FileNum) - } - return p.sharedPath(meta) -} - -// Size returns the size of the object. -func (p *Provider) Size(meta ObjectMetadata) (int64, error) { - if !meta.IsShared() { - return p.vfsSize(meta.FileType, meta.FileNum) - } - return p.sharedSize(meta) -} - -// List returns the objects currently known to the provider. Does not perform any I/O. -func (p *Provider) List() []ObjectMetadata { - p.mu.RLock() - defer p.mu.RUnlock() - res := make([]ObjectMetadata, 0, len(p.mu.knownObjects)) - for _, meta := range p.mu.knownObjects { - res = append(res, meta) - } - sort.Slice(res, func(i, j int) bool { - return res[i].FileNum < res[j].FileNum - }) - return res -} - -func (p *Provider) addMetadata(meta ObjectMetadata) { - p.mu.Lock() - defer p.mu.Unlock() - p.mu.knownObjects[meta.FileNum] = meta - if meta.IsShared() { - p.mu.shared.catalogBatch.AddObject(sharedobjcat.SharedObjectMetadata{ - FileNum: meta.FileNum, - FileType: meta.FileType, - CreatorID: meta.Shared.CreatorID, - CreatorFileNum: meta.Shared.CreatorFileNum, - }) - } else { - p.mu.localObjectsChanged = true - } -} - -func (p *Provider) removeMetadata(fileNum base.FileNum) { - p.mu.Lock() - defer p.mu.Unlock() - - meta, ok := p.mu.knownObjects[fileNum] - if !ok { - return - } - delete(p.mu.knownObjects, fileNum) - if meta.IsShared() { - p.mu.shared.catalogBatch.DeleteObject(fileNum) - } else { - p.mu.localObjectsChanged = true - } -} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/remote/factory.go b/vendor/github.com/cockroachdb/pebble/objstorage/remote/factory.go new file mode 100644 index 0000000..620dbba --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/remote/factory.go @@ -0,0 +1,25 @@ +// Copyright 2023 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 remote + +import "github.com/pkg/errors" + +// MakeSimpleFactory returns a StorageFactory implementation that produces the given +// Storage objects. +func MakeSimpleFactory(m map[Locator]Storage) StorageFactory { + return simpleFactory(m) +} + +type simpleFactory map[Locator]Storage + +var _ StorageFactory = simpleFactory{} + +// CreateStorage is part of the StorageFactory interface. +func (sf simpleFactory) CreateStorage(locator Locator) (Storage, error) { + if s, ok := sf[locator]; ok { + return s, nil + } + return nil, errors.Errorf("unknown locator '%s'", locator) +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/remote/localfs.go b/vendor/github.com/cockroachdb/pebble/objstorage/remote/localfs.go new file mode 100644 index 0000000..539ecb1 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/remote/localfs.go @@ -0,0 +1,118 @@ +// Copyright 2023 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 remote + +import ( + "context" + "io" + "os" + "path" + + "github.com/cockroachdb/pebble/vfs" +) + +// NewLocalFS returns a vfs-backed implementation of the remote.Storage +// interface (for testing). All objects will be stored at the directory +// dirname. +func NewLocalFS(dirname string, fs vfs.FS) Storage { + store := &localFSStore{ + dirname: dirname, + vfs: fs, + } + return store +} + +// localFSStore is a vfs-backed implementation of the remote.Storage +// interface (for testing). +type localFSStore struct { + dirname string + vfs vfs.FS +} + +var _ Storage = (*localFSStore)(nil) + +// Close is part of the remote.Storage interface. +func (s *localFSStore) Close() error { + *s = localFSStore{} + return nil +} + +// ReadObject is part of the remote.Storage interface. +func (s *localFSStore) ReadObject( + ctx context.Context, objName string, +) (_ ObjectReader, objSize int64, _ error) { + f, err := s.vfs.Open(path.Join(s.dirname, objName)) + if err != nil { + return nil, 0, err + } + stat, err := f.Stat() + if err != nil { + return nil, 0, err + } + + return &localFSReader{f}, stat.Size(), nil +} + +type localFSReader struct { + file vfs.File +} + +var _ ObjectReader = (*localFSReader)(nil) + +// ReadAt is part of the shared.ObjectReader interface. +func (r *localFSReader) ReadAt(_ context.Context, p []byte, offset int64) error { + n, err := r.file.ReadAt(p, offset) + // https://pkg.go.dev/io#ReaderAt + if err == io.EOF && n == len(p) { + return nil + } + return err +} + +// Close is part of the shared.ObjectReader interface. +func (r *localFSReader) Close() error { + r.file.Close() + r.file = nil + return nil +} + +// CreateObject is part of the remote.Storage interface. +func (s *localFSStore) CreateObject(objName string) (io.WriteCloser, error) { + file, err := s.vfs.Create(path.Join(s.dirname, objName)) + return file, err +} + +// List is part of the remote.Storage interface. +func (s *localFSStore) List(prefix, delimiter string) ([]string, error) { + // TODO(josh): For the intended use case of localfs.go of running 'pebble bench', + // List can always return , since this indicates a file has only one ref, + // and since `pebble bench` implies running in a single-pebble-instance context. + // https://github.com/cockroachdb/pebble/blob/a9a079d4fb6bf4a9ebc52e4d83a76ad4cbf676cb/objstorage/objstorageprovider/shared.go#L292 + return nil, nil +} + +// Delete is part of the remote.Storage interface. +func (s *localFSStore) Delete(objName string) error { + return s.vfs.Remove(path.Join(s.dirname, objName)) +} + +// Size is part of the remote.Storage interface. +func (s *localFSStore) Size(objName string) (int64, error) { + f, err := s.vfs.Open(path.Join(s.dirname, objName)) + if err != nil { + return 0, err + } + defer f.Close() + stat, err := f.Stat() + if err != nil { + return 0, err + } + return stat.Size(), nil +} + +// IsNotExistError is part of the remote.Storage interface. +func (s *localFSStore) IsNotExistError(err error) bool { + return err == os.ErrNotExist +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/shared/logging.go b/vendor/github.com/cockroachdb/pebble/objstorage/remote/logging.go similarity index 57% rename from vendor/github.com/cockroachdb/pebble/objstorage/shared/logging.go rename to vendor/github.com/cockroachdb/pebble/objstorage/remote/logging.go index 5449d11..7a9cd77 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/shared/logging.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/remote/logging.go @@ -2,9 +2,10 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -package shared +package remote import ( + "context" "fmt" "io" "sort" @@ -19,7 +20,7 @@ func WithLogging(wrapped Storage, logf func(fmt string, args ...interface{})) St } } -// loggingStore wraps a shared.Storage implementation and emits logs of the +// loggingStore wraps a remote.Storage implementation and emits logs of the // operations. type loggingStore struct { logf func(fmt string, args ...interface{}) @@ -33,41 +34,52 @@ func (l *loggingStore) Close() error { return l.wrapped.Close() } -func (l *loggingStore) ReadObjectAt( - basename string, offset int64, -) (_ io.ReadCloser, totalSize int64, _ error) { - r, totalSize, err := l.wrapped.ReadObjectAt(basename, offset) - l.logf("read object %q at %d: %s", basename, offset, errOrPrintf(err, "%d bytes", totalSize)) +func (l *loggingStore) ReadObject( + ctx context.Context, objName string, +) (_ ObjectReader, objSize int64, _ error) { + r, size, err := l.wrapped.ReadObject(ctx, objName) + l.logf("create reader for object %q: %s", objName, errOrPrintf(err, "%d bytes", size)) if err != nil { return nil, 0, err } return &loggingReader{ - l: l, - name: basename, - ReadCloser: r, - }, totalSize, nil + l: l, + name: objName, + wrapped: r, + }, size, nil } type loggingReader struct { - l *loggingStore - name string - io.ReadCloser + l *loggingStore + name string + wrapped ObjectReader +} + +var _ ObjectReader = (*loggingReader)(nil) + +func (l *loggingReader) ReadAt(ctx context.Context, p []byte, offset int64) error { + if err := l.wrapped.ReadAt(ctx, p, offset); err != nil { + l.l.logf("read object %q at %d (length %d): error %v", l.name, offset, len(p), err) + return err + } + l.l.logf("read object %q at %d (length %d)", l.name, offset, len(p)) + return nil } func (l *loggingReader) Close() error { l.l.logf("close reader for %q", l.name) - return l.ReadCloser.Close() + return l.wrapped.Close() } -func (l *loggingStore) CreateObject(basename string) (io.WriteCloser, error) { - l.logf("create object %q", basename) - writer, err := l.wrapped.CreateObject(basename) +func (l *loggingStore) CreateObject(objName string) (io.WriteCloser, error) { + l.logf("create object %q", objName) + writer, err := l.wrapped.CreateObject(objName) if err != nil { return nil, err } return &loggingWriter{ l: l, - name: basename, + name: objName, WriteCloser: writer, }, nil } @@ -104,14 +116,14 @@ func (l *loggingStore) List(prefix, delimiter string) ([]string, error) { return res, nil } -func (l *loggingStore) Delete(basename string) error { - l.logf("delete object %q", basename) - return l.wrapped.Delete(basename) +func (l *loggingStore) Delete(objName string) error { + l.logf("delete object %q", objName) + return l.wrapped.Delete(objName) } -func (l *loggingStore) Size(basename string) (int64, error) { - size, err := l.wrapped.Size(basename) - l.logf("size of object %q: %s", basename, errOrPrintf(err, "%d", size)) +func (l *loggingStore) Size(objName string) (int64, error) { + size, err := l.wrapped.Size(objName) + l.logf("size of object %q: %s", objName, errOrPrintf(err, "%d", size)) return size, err } @@ -121,3 +133,7 @@ func errOrPrintf(err error, format string, args ...interface{}) string { } return fmt.Sprintf(format, args...) } + +func (l *loggingStore) IsNotExistError(err error) bool { + return l.wrapped.IsNotExistError(err) +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/shared/mem.go b/vendor/github.com/cockroachdb/pebble/objstorage/remote/mem.go similarity index 69% rename from vendor/github.com/cockroachdb/pebble/objstorage/shared/mem.go rename to vendor/github.com/cockroachdb/pebble/objstorage/remote/mem.go index 72cc7c0..9bbda71 100644 --- a/vendor/github.com/cockroachdb/pebble/objstorage/shared/mem.go +++ b/vendor/github.com/cockroachdb/pebble/objstorage/remote/mem.go @@ -2,17 +2,18 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -package shared +package remote import ( "bytes" + "context" "io" "os" "strings" "sync" ) -// NewInMem returns an in-memory implementation of the shared.Storage +// NewInMem returns an in-memory implementation of the remote.Storage // interface (for testing). func NewInMem() Storage { store := &inMemStore{} @@ -20,7 +21,7 @@ func NewInMem() Storage { return store } -// inMemStore is an in-memory implementation of the shared.Storage interface +// inMemStore is an in-memory implementation of the remote.Storage interface // (for testing). type inMemStore struct { mu struct { @@ -41,39 +42,39 @@ func (s *inMemStore) Close() error { return nil } -func (s *inMemStore) ReadObjectAt(basename string, offset int64) (io.ReadCloser, int64, error) { - obj, err := s.getObj(basename) +func (s *inMemStore) ReadObject( + ctx context.Context, objName string, +) (_ ObjectReader, objSize int64, _ error) { + obj, err := s.getObj(objName) if err != nil { return nil, 0, err } - remaining := int64(len(obj.data)) - offset - if remaining < 0 { - return nil, 0, io.EOF - } - return newInMemReader(obj.data[int(offset):]), remaining, nil + return &inMemReader{data: obj.data}, int64(len(obj.data)), nil } type inMemReader struct { - bytes.Reader + data []byte } -func newInMemReader(b []byte) *inMemReader { - r := &inMemReader{} - r.Reset(b) - return r -} +var _ ObjectReader = (*inMemReader)(nil) -var _ io.ReadCloser = (*inMemReader)(nil) - -func (r *inMemReader) Close() error { - r.Reset(nil) +func (r *inMemReader) ReadAt(ctx context.Context, p []byte, offset int64) error { + if offset+int64(len(p)) > int64(len(r.data)) { + return io.EOF + } + copy(p, r.data[offset:]) return nil } -func (s *inMemStore) CreateObject(basename string) (io.WriteCloser, error) { +func (r *inMemReader) Close() error { + r.data = nil + return nil +} + +func (s *inMemStore) CreateObject(objName string) (io.WriteCloser, error) { return &inMemWriter{ store: s, - name: basename, + name: objName, }, nil } @@ -119,20 +120,24 @@ func (s *inMemStore) List(prefix, delimiter string) ([]string, error) { return res, nil } -func (s *inMemStore) Delete(basename string) error { - s.rmObj(basename) +func (s *inMemStore) Delete(objName string) error { + s.rmObj(objName) return nil } // Size returns the length of the named object in bytesWritten. -func (s *inMemStore) Size(basename string) (int64, error) { - obj, err := s.getObj(basename) +func (s *inMemStore) Size(objName string) (int64, error) { + obj, err := s.getObj(objName) if err != nil { return 0, err } return int64(len(obj.data)), nil } +func (s *inMemStore) IsNotExistError(err error) bool { + return err == os.ErrNotExist +} + func (s *inMemStore) getObj(name string) (*inMemObj, error) { s.mu.Lock() defer s.mu.Unlock() diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/remote/storage.go b/vendor/github.com/cockroachdb/pebble/objstorage/remote/storage.go new file mode 100644 index 0000000..dccb006 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/objstorage/remote/storage.go @@ -0,0 +1,126 @@ +// Copyright 2023 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 remote + +import ( + "context" + "io" +) + +// Locator is an opaque string identifying a remote.Storage implementation. +// +// The Locator must not contain secrets (like authentication keys). Locators are +// stored on disk in the shared object catalog and are passed around as part of +// RemoteObjectBacking; they can also appear in error messages. +type Locator string + +// StorageFactory is used to return Storage implementations based on locators. A +// Pebble store that uses shared storage is configured with a StorageFactory. +type StorageFactory interface { + CreateStorage(locator Locator) (Storage, error) +} + +// SharedLevelsStart denotes the highest (i.e. lowest numbered) level that will +// have sstables shared across Pebble instances when doing skip-shared +// iteration (see db.ScanInternal) or shared file ingestion (see +// db.IngestAndExcise). +const SharedLevelsStart = 5 + +// CreateOnSharedStrategy specifies what table files should be created on shared +// storage. For use with CreateOnShared in options. +type CreateOnSharedStrategy int + +const ( + // CreateOnSharedNone denotes no files being created on shared storage. + CreateOnSharedNone CreateOnSharedStrategy = iota + // CreateOnSharedLower denotes the creation of files in lower levels of the + // LSM (specifically, L5 and L6 as they're below SharedLevelsStart) on + // shared storage, and higher levels on local storage. + CreateOnSharedLower + // CreateOnSharedAll denotes the creation of all sstables on shared storage. + CreateOnSharedAll +) + +// ShouldCreateShared returns whether new table files at the specified level +// should be created on shared storage. +func ShouldCreateShared(strategy CreateOnSharedStrategy, level int) bool { + switch strategy { + case CreateOnSharedAll: + return true + case CreateOnSharedNone: + return false + case CreateOnSharedLower: + return level >= SharedLevelsStart + default: + panic("unexpected CreateOnSharedStrategy value") + } +} + +// Storage is an interface for a blob storage driver. This is lower-level +// than an FS-like interface, however FS/File-like abstractions can be built on +// top of these methods. +// +// TODO(bilal): Consider pushing shared file obsoletion as well as path +// generation behind this interface. +type Storage interface { + io.Closer + + // ReadObject returns an ObjectReader that can be used to perform reads on an + // object, along with the total size of the object. + ReadObject(ctx context.Context, objName string) (_ ObjectReader, objSize int64, _ error) + + // CreateObject returns a writer for the object at the request name. A new + // empty object is created if CreateObject is called on an existing object. + // + // A Writer *must* be closed via either Close, and if closing returns a + // non-nil error, that error should be handled or reported to the user -- an + // implementation may buffer written data until Close and only then return + // an error, or Write may return an opaque io.EOF with the underlying cause + // returned by the subsequent Close(). + // + // TODO(radu): if we encounter some unrelated error while writing to the + // WriteCloser, we'd want to abort the whole thing rather than letting Close + // finalize the upload. + CreateObject(objName string) (io.WriteCloser, error) + + // List enumerates files within the supplied prefix, returning a list of + // objects within that prefix. If delimiter is non-empty, names which have the + // same prefix, prior to the delimiter but after the prefix, are grouped into a + // single result which is that prefix. The order that results are returned is + // undefined. If a prefix is specified, the prefix is trimmed from the result + // list. + // + // An example would be, if the storage contains objects a, b/4, b/5 and b/6, + // these would be the return values: + // List("", "") -> ["a", "b/4", "b/5", "b/6"] + // List("", "/") -> ["a", "b"] + // List("b", "/") -> ["4", "5", "6"] + // List("b", "") -> ["/4", "/5", "/6"] + List(prefix, delimiter string) ([]string, error) + + // Delete removes the named object from the store. + Delete(objName string) error + + // Size returns the length of the named object in bytesWritten. + Size(objName string) (int64, error) + + // IsNotExistError returns true if the given error (returned by a method in + // this interface) indicates that the object does not exist. + IsNotExistError(err error) bool +} + +// ObjectReader is used to perform reads on an object. +type ObjectReader interface { + // ReadAt reads len(p) bytes into p starting at offset off. + // + // Does not return partial results; if offset + len(p) is past the end of the + // object, an error is returned. + // + // Clients of ReadAt can execute parallel ReadAt calls on the same + // ObjectReader. + ReadAt(ctx context.Context, p []byte, offset int64) error + + Close() error +} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/shared.go b/vendor/github.com/cockroachdb/pebble/objstorage/shared.go deleted file mode 100644 index 5d9652e..0000000 --- a/vendor/github.com/cockroachdb/pebble/objstorage/shared.go +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2023 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 objstorage - -import ( - "fmt" - "sync" - "sync/atomic" - - "github.com/cockroachdb/errors" - "github.com/cockroachdb/pebble/internal/base" - "github.com/cockroachdb/pebble/objstorage/sharedobjcat" -) - -// sharedSubsystem contains the provider fields related to shared storage. -// All fields remain unset if shared storage is not configured. -type sharedSubsystem struct { - catalog *sharedobjcat.Catalog - // initialized guards access to the creatorID field. - initialized atomic.Bool - creatorID CreatorID - initOnce sync.Once -} - -func (ss *sharedSubsystem) init(creatorID CreatorID) { - ss.initOnce.Do(func() { - ss.creatorID = creatorID - ss.initialized.Store(true) - }) -} - -// sharedInit initializes the shared object subsystem (if configured) and finds -// any shared objects. -func (p *Provider) sharedInit() error { - if p.st.Shared.Storage == nil { - return nil - } - catalog, contents, err := sharedobjcat.Open(p.st.FS, p.st.FSDirName) - if err != nil { - return errors.Wrapf(err, "pebble: could not open shared object catalog") - } - p.shared.catalog = catalog - - // The creator ID may or may not be initialized yet. - if contents.CreatorID.IsSet() { - p.shared.init(contents.CreatorID) - p.st.Logger.Infof("shared storage configured; creatorID = %s", contents.CreatorID) - } else { - p.st.Logger.Infof("shared storage configured; no creatorID yet") - } - - for _, meta := range contents.Objects { - o := ObjectMetadata{ - FileNum: meta.FileNum, - FileType: meta.FileType, - } - o.Shared.CreatorID = meta.CreatorID - o.Shared.CreatorFileNum = meta.CreatorFileNum - p.mu.knownObjects[o.FileNum] = o - } - return nil -} - -// SetCreatorID sets the CreatorID which is needed in order to use shared -// objects. Shared object usage is disabled until this method is called the -// first time. Once set, the Creator ID is persisted and cannot change. -// -// Cannot be called if shared storage is not configured for the provider. -func (p *Provider) SetCreatorID(creatorID CreatorID) error { - if p.st.Shared.Storage == nil { - return errors.AssertionFailedf("attempt to set CreatorID but shared storage not enabled") - } - // Note: this call is a cheap no-op if the creator ID was already set. This - // call also checks if we are trying to change the ID. - if err := p.shared.catalog.SetCreatorID(creatorID); err != nil { - return err - } - if !p.shared.initialized.Load() { - p.st.Logger.Infof("shared storage creatorID set to %s", creatorID) - p.shared.init(creatorID) - } - return nil -} - -func (p *Provider) sharedCheckInitialized() error { - if p.st.Shared.Storage == nil { - return errors.Errorf("shared object support not configured") - } - if !p.shared.initialized.Load() { - return errors.Errorf("shared object support not available: shared creator ID not yet set") - } - return nil -} - -func (p *Provider) sharedSync() error { - batch := func() sharedobjcat.Batch { - p.mu.Lock() - defer p.mu.Unlock() - res := p.mu.shared.catalogBatch.Copy() - p.mu.shared.catalogBatch.Reset() - return res - }() - - if batch.IsEmpty() { - return nil - } - - err := p.shared.catalog.ApplyBatch(batch) - if err != nil { - // We have to put back the batch (for the next Sync). - p.mu.Lock() - defer p.mu.Unlock() - batch.Append(p.mu.shared.catalogBatch) - p.mu.shared.catalogBatch = batch - return err - } - - return nil -} - -func (p *Provider) sharedPath(meta ObjectMetadata) string { - return "shared://" + sharedObjectName(meta) -} - -func sharedObjectName(meta ObjectMetadata) string { - // TODO(radu): prepend a "shard" value for better distribution within the bucket? - return fmt.Sprintf( - "%s-%s", - meta.Shared.CreatorID, base.MakeFilename(meta.FileType, meta.Shared.CreatorFileNum), - ) -} - -func (p *Provider) sharedCreate( - fileType base.FileType, fileNum base.FileNum, -) (Writable, ObjectMetadata, error) { - if err := p.sharedCheckInitialized(); err != nil { - return nil, ObjectMetadata{}, err - } - meta := ObjectMetadata{ - FileNum: fileNum, - FileType: fileType, - } - meta.Shared.CreatorID = p.shared.creatorID - meta.Shared.CreatorFileNum = fileNum - - objName := sharedObjectName(meta) - writer, err := p.st.Shared.Storage.CreateObject(objName) - if err != nil { - return nil, ObjectMetadata{}, err - } - return &sharedWritable{ - storageWriter: writer, - }, meta, nil -} - -func (p *Provider) sharedOpenForReading(meta ObjectMetadata) (Readable, error) { - if err := p.sharedCheckInitialized(); err != nil { - return nil, err - } - objName := sharedObjectName(meta) - size, err := p.st.Shared.Storage.Size(objName) - if err != nil { - return nil, err - } - return newSharedReadable(p.st.Shared.Storage, objName, size), nil -} - -func (p *Provider) sharedSize(meta ObjectMetadata) (int64, error) { - if err := p.sharedCheckInitialized(); err != nil { - return 0, err - } - objName := sharedObjectName(meta) - return p.st.Shared.Storage.Size(objName) -} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/shared/storage.go b/vendor/github.com/cockroachdb/pebble/objstorage/shared/storage.go deleted file mode 100644 index ae8a516..0000000 --- a/vendor/github.com/cockroachdb/pebble/objstorage/shared/storage.go +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2023 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 shared - -import "io" - -// Storage is an interface for a blob storage driver. This is lower-level -// than an FS-like interface, however FS/File-like abstractions can be built on -// top of these methods. -// -// TODO(bilal): Consider pushing shared file obsoletion as well as path -// generation behind this interface. -type Storage interface { - io.Closer - - // ReadObjectAt returns a Reader for reading the object at the requested name - // and offset, along with the total size of the object. - ReadObjectAt(basename string, offset int64) (_ io.ReadCloser, totalSize int64, _ error) - - // CreateObject returns a writer for the object at the request name. A new - // empty object is created if CreateObject is called on an existing object. - // - // A Writer *must* be closed via either Close, and if closing returns a - // non-nil error, that error should be handled or reported to the user -- an - // implementation may buffer written data until Close and only then return - // an error, or Write may return an opaque io.EOF with the underlying cause - // returned by the subsequent Close(). - // - // TODO(radu): if we encounter some unrelated error while writing to the - // WriteCloser, we'd want to abort the whole thing rather than letting Close - // finalize the upload. - CreateObject(basename string) (io.WriteCloser, error) - - // List enumerates files within the supplied prefix, returning a list of - // objects within that prefix. If delimiter is non-empty, names which have the - // same prefix, prior to the delimiter but after the prefix, are grouped into a - // single result which is that prefix. The order that results are returned is - // undefined. If a prefix is specified, the prefix is trimmed from the result - // list. - // - // An example would be, if the storage contains objects a, b/4, b/5 and b/6, - // these would be the return values: - // List("", "") -> ["a", "b/4", "b/5", "b/6"] - // List("", "/") -> ["a", "b"] - // List("b", "/") -> ["4", "5", "6"] - // List("b", "") -> ["/4", "/5", "/6"] - List(prefix, delimiter string) ([]string, error) - - // Delete removes the named object from the store. - // - // TODO(radu): do we get an error when the object isn't there? How can we - // check for that error? - Delete(basename string) error - - // Size returns the length of the named object in bytesWritten. - // - // TODO(radu): same as above - how can we tell if it's a "no such object" error? - Size(basename string) (int64, error) -} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/shared_backing.go b/vendor/github.com/cockroachdb/pebble/objstorage/shared_backing.go deleted file mode 100644 index cacf898..0000000 --- a/vendor/github.com/cockroachdb/pebble/objstorage/shared_backing.go +++ /dev/null @@ -1,145 +0,0 @@ -// Copyright 2023 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 objstorage - -import ( - "bytes" - "encoding/binary" - "io" - - "github.com/cockroachdb/errors" - "github.com/cockroachdb/pebble/internal/base" - "github.com/cockroachdb/pebble/objstorage/sharedobjcat" -) - -// SharedObjectBacking encodes the metadata necessary to incorporate a shared -// object into a different Pebble instance. -type SharedObjectBacking []byte - -const ( - tagCreatorID = 1 - tagCreatorFileNum = 2 - - // Any new tags that don't have the tagNotSafeToIgnoreMask bit set must be - // followed by the length of the data (so they can be skipped). - - // Any new tags that have the tagNotSafeToIgnoreMask bit set cause errors if - // they are encountered by earlier code that doesn't know the tag. - tagNotSafeToIgnoreMask = 64 -) - -// SharedObjectBacking encodes the shared object metadata. -func (meta *ObjectMetadata) SharedObjectBacking() (SharedObjectBacking, error) { - if !meta.IsShared() { - return nil, errors.AssertionFailedf("object %s not on shared storage", meta.FileNum) - } - - buf := make([]byte, 0, binary.MaxVarintLen64*4) - buf = binary.AppendUvarint(buf, tagCreatorID) - buf = binary.AppendUvarint(buf, uint64(meta.Shared.CreatorID)) - buf = binary.AppendUvarint(buf, tagCreatorFileNum) - buf = binary.AppendUvarint(buf, uint64(meta.Shared.CreatorFileNum)) - return buf, nil -} - -// fromSharedObjectBacking decodes the shared object metadata. -func fromSharedObjectBacking( - fileType base.FileType, fileNum base.FileNum, buf SharedObjectBacking, -) (ObjectMetadata, error) { - var creatorID uint64 - var creatorFileNum uint64 - br := bytes.NewReader(buf) - for { - tag, err := binary.ReadUvarint(br) - if err == io.EOF { - break - } - if err != nil { - return ObjectMetadata{}, err - } - switch tag { - case tagCreatorID: - creatorID, err = binary.ReadUvarint(br) - - case tagCreatorFileNum: - creatorFileNum, err = binary.ReadUvarint(br) - - // TODO(radu): encode file type as well? - - default: - // Ignore unknown tags, unless they're not safe to ignore. - if tag&tagNotSafeToIgnoreMask != 0 { - return ObjectMetadata{}, errors.Newf("unknown tag %d", tag) - } - var dataLen uint64 - dataLen, err = binary.ReadUvarint(br) - if err == nil { - _, err = br.Seek(int64(dataLen), io.SeekCurrent) - } - } - if err != nil { - return ObjectMetadata{}, err - } - } - if creatorID == 0 { - return ObjectMetadata{}, errors.Newf("shared object backing missing creator ID") - } - if creatorFileNum == 0 { - return ObjectMetadata{}, errors.Newf("shared object backing missing creator file num") - } - meta := ObjectMetadata{ - FileNum: fileNum, - FileType: fileType, - } - meta.Shared.CreatorID = CreatorID(creatorID) - meta.Shared.CreatorFileNum = base.FileNum(creatorFileNum) - return meta, nil -} - -// SharedObjectToAttach contains the arguments needed to attach an existing shared object. -type SharedObjectToAttach struct { - // FileNum is the file number that will be used to refer to this object (in - // the context of this instance). - FileNum base.FileNum - FileType base.FileType - // Backing contains the metadata for the share dobject backing (normally - // generated from a different instance). - Backing SharedObjectBacking -} - -// AttachSharedObjects registers existing shared objects with this provider. -func (p *Provider) AttachSharedObjects(objs []SharedObjectToAttach) ([]ObjectMetadata, error) { - metas := make([]ObjectMetadata, len(objs)) - for i, o := range objs { - meta, err := fromSharedObjectBacking(o.FileType, o.FileNum, o.Backing) - if err != nil { - return nil, err - } - metas[i] = meta - } - - func() { - p.mu.Lock() - defer p.mu.Unlock() - for _, meta := range metas { - p.mu.shared.catalogBatch.AddObject(sharedobjcat.SharedObjectMetadata{ - FileNum: meta.FileNum, - FileType: meta.FileType, - CreatorID: meta.Shared.CreatorID, - CreatorFileNum: meta.Shared.CreatorFileNum, - }) - } - }() - if err := p.sharedSync(); err != nil { - return nil, err - } - - p.mu.Lock() - defer p.mu.Unlock() - for _, meta := range metas { - p.mu.knownObjects[meta.FileNum] = meta - } - return metas, nil -} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/shared_readable.go b/vendor/github.com/cockroachdb/pebble/objstorage/shared_readable.go deleted file mode 100644 index 5bd32f7..0000000 --- a/vendor/github.com/cockroachdb/pebble/objstorage/shared_readable.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2023 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 objstorage - -import ( - "io" - - "github.com/cockroachdb/pebble/objstorage/shared" -) - -// sharedReadable is a very simple implementation of Readable on top of the -// ReadCloser returned by shared.Storage.CreateObject. -type sharedReadable struct { - storage shared.Storage - objName string - size int64 - - // rh is used for direct ReadAt calls without a read handle. - rh sharedReadHandle -} - -var _ Readable = (*sharedReadable)(nil) - -func newSharedReadable(storage shared.Storage, objName string, size int64) *sharedReadable { - r := &sharedReadable{ - storage: storage, - objName: objName, - size: size, - } - r.rh.readable = r - return r -} - -func (r *sharedReadable) ReadAt(p []byte, offset int64) (n int, err error) { - return r.rh.ReadAt(p, offset) -} - -func (r *sharedReadable) Close() error { - err := r.rh.Close() - r.storage = nil - return err -} - -func (r *sharedReadable) Size() int64 { - return r.size -} - -func (r *sharedReadable) NewReadHandle() ReadHandle { - // TODO(radu): use a pool. - return &sharedReadHandle{readable: r} -} - -type sharedReadHandle struct { - readable *sharedReadable - lastReader io.ReadCloser - lastOffset int64 -} - -var _ ReadHandle = (*sharedReadHandle)(nil) - -func (r *sharedReadHandle) ReadAt(p []byte, offset int64) (n int, err error) { - // See if this continues the previous read so that we can reuse the last reader. - if r.lastReader == nil || r.lastOffset != offset { - // We need to create a new reader. - if r.lastReader != nil { - if err := r.lastReader.Close(); err != nil { - return 0, err - } - r.lastReader = nil - } - reader, _, err := r.readable.storage.ReadObjectAt(r.readable.objName, offset) - if err != nil { - return 0, err - } - r.lastReader = reader - r.lastOffset = offset - } - n, err = io.ReadFull(r.lastReader, p) - r.lastOffset += int64(n) - return n, err -} - -func (r *sharedReadHandle) Close() error { - var err error - if r.lastReader != nil { - err = r.lastReader.Close() - r.lastReader = nil - } - r.readable = nil - return err -} - -func (r *sharedReadHandle) MaxReadahead() {} - -func (r *sharedReadHandle) RecordCacheHit(offset, size int64) {} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/shared_writable.go b/vendor/github.com/cockroachdb/pebble/objstorage/shared_writable.go deleted file mode 100644 index 2c5e444..0000000 --- a/vendor/github.com/cockroachdb/pebble/objstorage/shared_writable.go +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2023 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 objstorage - -import "io" - -// sharedWritable is a very simple implementation of Writable on top of the -// WriteCloser returned by shared.Storage.CreateObject. -type sharedWritable struct { - storageWriter io.WriteCloser -} - -var _ Writable = (*sharedWritable)(nil) - -// Write is part of the Writable interface. -func (w *sharedWritable) Write(p []byte) error { - _, err := w.storageWriter.Write(p) - return err -} - -// Finish is part of the Writable interface. -func (w *sharedWritable) Finish() error { - err := w.storageWriter.Close() - w.storageWriter = nil - return err -} - -// Abort is part of the Writable interface. -func (w *sharedWritable) Abort() { - _ = w.storageWriter.Close() - w.storageWriter = nil -} diff --git a/vendor/github.com/cockroachdb/pebble/objstorage/sharedobjcat/version_edit.go b/vendor/github.com/cockroachdb/pebble/objstorage/sharedobjcat/version_edit.go deleted file mode 100644 index e6da72b..0000000 --- a/vendor/github.com/cockroachdb/pebble/objstorage/sharedobjcat/version_edit.go +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2023 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 sharedobjcat - -import ( - "bufio" - "encoding/binary" - "io" - - "github.com/cockroachdb/errors" - "github.com/cockroachdb/pebble/internal/base" -) - -// versionEdit is a modification to the shared object state which can be encoded -// into a record. -// -// TODO(radu): consider adding creation and deletion time for debugging purposes. -type versionEdit struct { - NewObjects []SharedObjectMetadata - DeletedObjects []base.FileNum - CreatorID CreatorID -} - -const ( - // tagNewObject is followed by the FileNum, creator ID and creator FileNum. - tagNewObject = 1 - // tagDeletedObject is followed by the FileNum. - tagDeletedObject = 2 - // tagCreatorID is followed by the Creator ID for this store. This ID can - // never change. - tagCreatorID = 3 -) - -// Object type values. We don't want to encode FileType directly because it is -// more general (and we want freedom to change it in the future). -const ( - objTypeTable = 1 -) - -func objTypeToFileType(objType uint64) (base.FileType, error) { - switch objType { - case objTypeTable: - return base.FileTypeTable, nil - default: - return 0, errors.Newf("unknown object type %d", objType) - } -} - -func fileTypeToObjType(fileType base.FileType) (uint64, error) { - switch fileType { - case base.FileTypeTable: - return objTypeTable, nil - - default: - return 0, errors.Newf("unknown object type for file type %d", fileType) - } -} - -// Encode encodes an edit to the specified writer. -func (v *versionEdit) Encode(w io.Writer) error { - buf := make([]byte, 0, binary.MaxVarintLen64*(len(v.NewObjects)*4+len(v.DeletedObjects)*2+2)) - for _, meta := range v.NewObjects { - objType, err := fileTypeToObjType(meta.FileType) - if err != nil { - return err - } - buf = binary.AppendUvarint(buf, uint64(tagNewObject)) - buf = binary.AppendUvarint(buf, uint64(meta.FileNum)) - buf = binary.AppendUvarint(buf, objType) - buf = binary.AppendUvarint(buf, uint64(meta.CreatorID)) - buf = binary.AppendUvarint(buf, uint64(meta.CreatorFileNum)) - } - - for _, fileNum := range v.DeletedObjects { - buf = binary.AppendUvarint(buf, uint64(tagDeletedObject)) - buf = binary.AppendUvarint(buf, uint64(fileNum)) - } - if v.CreatorID.IsSet() { - buf = binary.AppendUvarint(buf, uint64(tagCreatorID)) - buf = binary.AppendUvarint(buf, uint64(v.CreatorID)) - } - _, err := w.Write(buf) - return err -} - -// Decode decodes an edit from the specified reader. -func (v *versionEdit) Decode(r io.Reader) error { - br, ok := r.(io.ByteReader) - if !ok { - br = bufio.NewReader(r) - } - for { - tag, err := binary.ReadUvarint(br) - if err == io.EOF { - break - } - if err != nil { - return err - } - - err = nil - switch tag { - case tagNewObject: - var fileNum, creatorID, creatorFileNum uint64 - var fileType base.FileType - fileNum, err = binary.ReadUvarint(br) - if err == nil { - var objType uint64 - objType, err = binary.ReadUvarint(br) - if err == nil { - fileType, err = objTypeToFileType(objType) - } - } - if err == nil { - creatorID, err = binary.ReadUvarint(br) - } - if err == nil { - creatorFileNum, err = binary.ReadUvarint(br) - } - if err == nil { - v.NewObjects = append(v.NewObjects, SharedObjectMetadata{ - FileNum: base.FileNum(fileNum), - FileType: fileType, - CreatorID: CreatorID(creatorID), - CreatorFileNum: base.FileNum(creatorFileNum), - }) - } - - case tagDeletedObject: - var fileNum uint64 - fileNum, err = binary.ReadUvarint(br) - if err == nil { - v.DeletedObjects = append(v.DeletedObjects, base.FileNum(fileNum)) - } - - case tagCreatorID: - var id uint64 - id, err = binary.ReadUvarint(br) - if err == nil { - v.CreatorID = CreatorID(id) - } - - default: - // Ignore unknown tags. - } - if err != nil { - if err == io.EOF { - return errCorruptCatalog - } - return err - } - } - return nil -} - -var errCorruptCatalog = base.CorruptionErrorf("pebble: corrupt shared object catalog") diff --git a/vendor/github.com/cockroachdb/pebble/open.go b/vendor/github.com/cockroachdb/pebble/open.go index 519b285..89b7c96 100644 --- a/vendor/github.com/cockroachdb/pebble/open.go +++ b/vendor/github.com/cockroachdb/pebble/open.go @@ -6,6 +6,7 @@ package pebble import ( "bytes" + "context" "encoding/binary" "fmt" "io" @@ -20,11 +21,14 @@ import ( "github.com/cockroachdb/pebble/internal/arenaskl" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/cache" + "github.com/cockroachdb/pebble/internal/constants" "github.com/cockroachdb/pebble/internal/invariants" "github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/manual" "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" "github.com/cockroachdb/pebble/record" + "github.com/cockroachdb/pebble/sstable" "github.com/cockroachdb/pebble/vfs" "github.com/prometheus/client_golang/prometheus" ) @@ -34,11 +38,23 @@ const ( // The max batch size is limited by the uint32 offsets stored in // internal/batchskl.node, DeferredBatchOp, and flushableBatchEntry. - maxBatchSize = 4 << 30 // 4 GB + // + // We limit the size to MaxUint32 (just short of 4GB) so that the exclusive + // end of an allocation fits in uint32. + // + // On 32-bit systems, slices are naturally limited to MaxInt (just short of + // 2GB). + maxBatchSize = constants.MaxUint32OrInt // The max memtable size is limited by the uint32 offsets stored in // internal/arenaskl.node, DeferredBatchOp, and flushableBatchEntry. - maxMemTableSize = 4 << 30 // 4 GB + // + // We limit the size to MaxUint32 (just short of 4GB) so that the exclusive + // end of an allocation fits in uint32. + // + // On 32-bit systems, slices are naturally limited to MaxInt (just short of + // 2GB). + maxMemTableSize = constants.MaxUint32OrInt ) // TableCacheSize can be used to determine the table @@ -157,7 +173,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { merge: opts.Merger.Merge, split: opts.Comparer.Split, abbreviatedKey: opts.Comparer.AbbreviatedKey, - largeBatchThreshold: (opts.MemTableSize - int(memTableEmptySize)) / 2, + largeBatchThreshold: (opts.MemTableSize - uint64(memTableEmptySize)) / 2, fileLock: fileLock, dataDir: dataDir, walDir: walDir, @@ -166,8 +182,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { closedCh: make(chan struct{}), } d.mu.versions = &versionSet{} - d.atomic.diskAvailBytes = math.MaxUint64 - d.mu.versions.diskAvailBytes = d.getDiskAvailableBytesCached + d.diskAvailBytes.Store(math.MaxUint64) defer func() { // If an error or panic occurs during open, attempt to release the manually @@ -192,6 +207,9 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { t.arenaBuf = nil } } + if d.cleanupManager != nil { + d.cleanupManager.Close() + } if d.objProvider != nil { d.objProvider.Close() } @@ -202,34 +220,30 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { }() d.commit = newCommitPipeline(commitEnv{ - logSeqNum: &d.mu.versions.atomic.logSeqNum, - visibleSeqNum: &d.mu.versions.atomic.visibleSeqNum, + logSeqNum: &d.mu.versions.logSeqNum, + visibleSeqNum: &d.mu.versions.visibleSeqNum, apply: d.commitApply, write: d.commitWrite, }) - if d.opts.Experimental.MinDeletionRate > 0 { - d.deletionPacer = newDeletionPacer(int64(d.opts.Experimental.MinDeletionRate), d.getDeletionPacerInfo) - } else { - d.deletionPacer = nilPacer - } d.mu.nextJobID = 1 d.mu.mem.nextSize = opts.MemTableSize if d.mu.mem.nextSize > initialMemTableSize { d.mu.mem.nextSize = initialMemTableSize } - d.mu.mem.cond.L = &d.mu.Mutex - d.mu.cleaner.cond.L = &d.mu.Mutex d.mu.compact.cond.L = &d.mu.Mutex d.mu.compact.inProgress = make(map[*compaction]struct{}) d.mu.compact.noOngoingFlushStartTime = time.Now() d.mu.snapshots.init() - // logSeqNum is the next sequence number that will be assigned. Start - // assigning sequence numbers from 1 to match rocksdb. - d.mu.versions.atomic.logSeqNum = 1 + // logSeqNum is the next sequence number that will be assigned. + // Start assigning sequence numbers from base.SeqNumStart to leave + // room for reserved sequence numbers (see comments around + // SeqNumStart). + d.mu.versions.logSeqNum.Store(base.SeqNumStart) d.mu.formatVers.vers.Store(uint64(formatVersion)) d.mu.formatVers.marker = formatVersionMarker d.timeNow = time.Now + d.openedAt = d.timeNow() d.mu.Lock() defer d.mu.Unlock() @@ -246,7 +260,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { } // Create the DB. - if err := d.mu.versions.create(jobID, dirname, opts, manifestMarker, setCurrent, &d.mu.Mutex); err != nil { + if err := d.mu.versions.create(jobID, dirname, opts, manifestMarker, setCurrent, d.FormatMajorVersion, &d.mu.Mutex); err != nil { return nil, err } } else { @@ -254,11 +268,11 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { return nil, errors.Wrapf(ErrDBAlreadyExists, "dirname=%q", dirname) } // Load the version set. - if err := d.mu.versions.load(dirname, opts, manifestFileNum, manifestMarker, setCurrent, &d.mu.Mutex); err != nil { + if err := d.mu.versions.load(dirname, opts, manifestFileNum.FileNum(), manifestMarker, setCurrent, d.FormatMajorVersion, &d.mu.Mutex); err != nil { return nil, err } if opts.ErrorIfNotPristine { - liveFileNums := make(map[FileNum]struct{}) + liveFileNums := make(map[base.DiskFileNum]struct{}) d.mu.versions.addLiveFileNums(liveFileNums) if len(liveFileNums) != 0 { return nil, errors.Wrapf(ErrDBNotPristine, "dirname=%q", dirname) @@ -271,7 +285,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { // sequence number of the first batch that will be inserted. if !d.opts.ReadOnly { var entry *flushableEntry - d.mu.mem.mutable, entry = d.newMemTable(0 /* logNum */, d.mu.versions.atomic.logSeqNum) + d.mu.mem.mutable, entry = d.newMemTable(0 /* logNum */, d.mu.versions.logSeqNum.Load()) d.mu.mem.queue = append(d.mu.mem.queue, entry) } @@ -287,7 +301,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { } ls = append(ls, ls2...) } - providerSettings := objstorage.Settings{ + providerSettings := objstorageprovider.Settings{ Logger: opts.Logger, FS: opts.FS, FSDirName: dirname, @@ -296,13 +310,18 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { NoSyncOnClose: opts.NoSyncOnClose, BytesPerSync: opts.BytesPerSync, } - providerSettings.Shared.Storage = opts.Experimental.SharedStorage + providerSettings.Remote.StorageFactory = opts.Experimental.RemoteStorage + providerSettings.Remote.CreateOnShared = opts.Experimental.CreateOnShared + providerSettings.Remote.CreateOnSharedLocator = opts.Experimental.CreateOnSharedLocator + providerSettings.Remote.CacheSizeBytes = opts.Experimental.SecondaryCacheSizeBytes - d.objProvider, err = objstorage.Open(providerSettings) + d.objProvider, err = objstorageprovider.Open(providerSettings) if err != nil { return nil, err } + d.cleanupManager = openCleanupManager(opts, d.objProvider, d.onObsoleteTableDelete, d.getDeletionPacerInfo) + if manifestExists { curVersion := d.mu.versions.currentVersion() if err := checkConsistency(curVersion, dirname, d.objProvider); err != nil { @@ -331,21 +350,21 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { // Don't reuse any obsolete file numbers to avoid modifying an // ingested sstable's original external file. - if d.mu.versions.nextFileNum <= fn { - d.mu.versions.nextFileNum = fn + 1 + if d.mu.versions.nextFileNum <= fn.FileNum() { + d.mu.versions.nextFileNum = fn.FileNum() + 1 } switch ft { case fileTypeLog: - if fn >= d.mu.versions.minUnflushedLogNum { - logFiles = append(logFiles, fileNumAndName{fn, filename}) + if fn.FileNum() >= d.mu.versions.minUnflushedLogNum { + logFiles = append(logFiles, fileNumAndName{fn.FileNum(), filename}) } - if d.logRecycler.minRecycleLogNum <= fn { - d.logRecycler.minRecycleLogNum = fn + 1 + if d.logRecycler.minRecycleLogNum <= fn.FileNum() { + d.logRecycler.minRecycleLogNum = fn.FileNum() + 1 } case fileTypeOptions: - if previousOptionsFileNum < fn { - previousOptionsFileNum = fn + if previousOptionsFileNum < fn.FileNum() { + previousOptionsFileNum = fn.FileNum() previousOptionsFilename = filename } case fileTypeTemp, fileTypeOldTemp: @@ -362,6 +381,15 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { } } + // Ratchet d.mu.versions.nextFileNum ahead of all known objects in the + // objProvider. This avoids FileNum collisions with obsolete sstables. + objects := d.objProvider.List() + for _, obj := range objects { + if d.mu.versions.nextFileNum <= obj.DiskFileNum.FileNum() { + d.mu.versions.nextFileNum = obj.DiskFileNum.FileNum() + 1 + } + } + // Validate the most-recent OPTIONS file, if there is one. var strictWALTail bool if previousOptionsFilename != "" { @@ -387,11 +415,11 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { } toFlush = append(toFlush, flush...) d.mu.versions.markFileNumUsed(lf.num) - if d.mu.versions.atomic.logSeqNum < maxSeqNum { - d.mu.versions.atomic.logSeqNum = maxSeqNum + if d.mu.versions.logSeqNum.Load() < maxSeqNum { + d.mu.versions.logSeqNum.Store(maxSeqNum) } } - d.mu.versions.atomic.visibleSeqNum = d.mu.versions.atomic.logSeqNum + d.mu.versions.visibleSeqNum.Store(d.mu.versions.logSeqNum.Load()) if !d.opts.ReadOnly { // Create an empty .log file. @@ -417,8 +445,8 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { entry.readerUnrefLocked(true) } - newLogName := base.MakeFilepath(opts.FS, d.walDirname, fileTypeLog, newLogNum) - d.mu.log.queue = append(d.mu.log.queue, fileInfo{fileNum: newLogNum, fileSize: 0}) + newLogName := base.MakeFilepath(opts.FS, d.walDirname, fileTypeLog, newLogNum.DiskFileNum()) + d.mu.log.queue = append(d.mu.log.queue, fileInfo{fileNum: newLogNum.DiskFileNum(), fileSize: 0}) logFile, err := opts.FS.Create(newLogName) if err != nil { return nil, err @@ -469,7 +497,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { if !d.opts.ReadOnly { // Write the current options to disk. - d.optionsFileNum = d.mu.versions.getNextFileNum() + d.optionsFileNum = d.mu.versions.getNextFileNum().DiskFileNum() tmpPath := base.MakeFilepath(opts.FS, dirname, fileTypeTemp, d.optionsFileNum) optionsPath := base.MakeFilepath(opts.FS, dirname, fileTypeOptions, d.optionsFileNum) @@ -504,7 +532,7 @@ func Open(dirname string, opts *Options) (db *DB, _ error) { if !d.opts.ReadOnly { d.scanObsoleteFiles(ls) - d.deleteObsoleteFiles(jobID, true /* waitForOngoing */) + d.deleteObsoleteFiles(jobID) } else { // All the log files are obsolete. d.mu.versions.metrics.WAL.Files = int64(len(logFiles)) @@ -613,9 +641,9 @@ func GetVersion(dir string, fs vfs.FS) (string, error) { // processed, reset version. This is because rocksdb often // writes multiple options files without deleting previous ones. // Otherwise, skip parsing this options file. - if fn > lastOptionsSeen { + if fn.FileNum() > lastOptionsSeen { version = "" - lastOptionsSeen = fn + lastOptionsSeen = fn.FileNum() } else { continue } @@ -677,8 +705,15 @@ func (d *DB) replayWAL( rr = record.NewReader(file, logNum) offset int64 // byte offset in rr lastFlushOffset int64 + keysReplayed int64 // number of keys replayed + batchesReplayed int64 // number of batches replayed ) + // TODO(jackson): This function is interspersed with panics, in addition to + // corruption error propagation. Audit them to ensure we're truly only + // panicking where the error points to Pebble bug and not user or + // hardware-induced corruption. + if d.opts.ReadOnly { // In read-only mode, we replay directly into the mutable memtable which will // never be flushed. @@ -725,7 +760,7 @@ func (d *DB) replayWAL( // TODO(bananabrick): See if we can use the actual base level here, // instead of using 1. c := newFlush(d.opts, d.mu.versions.currentVersion(), - 1 /* base level */, toFlush) + 1 /* base level */, toFlush, d.timeNow()) newVE, _, _, err := d.runCompaction(jobID, c) if err != nil { return errors.Wrapf(err, "running compaction during WAL replay") @@ -733,6 +768,11 @@ func (d *DB) replayWAL( ve.NewFiles = append(ve.NewFiles, newVE.NewFiles...) return nil } + defer func() { + if err != nil { + err = errors.WithDetailf(err, "replaying log %s, offset %d", logNum, offset) + } + }() for { offset = rr.Offset() @@ -765,26 +805,33 @@ func (d *DB) replayWAL( // Specify Batch.db so that Batch.SetRepr will compute Batch.memTableSize // which is used below. - b = Batch{db: d} + b = Batch{} + b.db = d b.SetRepr(buf.Bytes()) seqNum := b.SeqNum() maxSeqNum = seqNum + uint64(b.Count()) - + keysReplayed += int64(b.Count()) + batchesReplayed++ { br := b.Reader() - if kind, encodedFileNum, _, _ := br.Next(); kind == InternalKeyKindIngestSST { - fileNums := make([]FileNum, 0, b.Count()) + if kind, encodedFileNum, _, ok, err := br.Next(); err != nil { + return nil, 0, err + } else if ok && kind == InternalKeyKindIngestSST { + fileNums := make([]base.DiskFileNum, 0, b.Count()) addFileNum := func(encodedFileNum []byte) { fileNum, n := binary.Uvarint(encodedFileNum) if n <= 0 { panic("pebble: ingest sstable file num is invalid.") } - fileNums = append(fileNums, base.FileNum(fileNum)) + fileNums = append(fileNums, base.FileNum(fileNum).DiskFileNum()) } addFileNum(encodedFileNum) for i := 1; i < int(b.Count()); i++ { - kind, encodedFileNum, _, ok := br.Next() + kind, encodedFileNum, _, ok, err := br.Next() + if err != nil { + return nil, 0, err + } if kind != InternalKeyKindIngestSST { panic("pebble: invalid batch key kind.") } @@ -794,21 +841,41 @@ func (d *DB) replayWAL( addFileNum(encodedFileNum) } - if _, _, _, ok := br.Next(); ok { + if _, _, _, ok, err := br.Next(); err != nil { + return nil, 0, err + } else if ok { panic("pebble: invalid number of entries in batch.") } - paths := make([]string, len(fileNums)) + meta := make([]*fileMetadata, len(fileNums)) for i, n := range fileNums { - paths[i] = base.MakeFilepath(d.opts.FS, d.dirname, fileTypeTable, n) - } + var readable objstorage.Readable + objMeta, err := d.objProvider.Lookup(fileTypeTable, n) + if err != nil { + return nil, 0, errors.Wrap(err, "pebble: error when looking up ingested SSTs") + } + if objMeta.IsRemote() { + readable, err = d.objProvider.OpenForReading(context.TODO(), fileTypeTable, n, objstorage.OpenOptions{MustExist: true}) + if err != nil { + return nil, 0, errors.Wrap(err, "pebble: error when opening flushable ingest files") + } + } else { + path := base.MakeFilepath(d.opts.FS, d.dirname, fileTypeTable, n) + f, err := d.opts.FS.Open(path) + if err != nil { + return nil, 0, err + } - var meta []*manifest.FileMetadata - meta, _, err = ingestLoad( - d.opts, d.FormatMajorVersion(), paths, d.cacheID, fileNums, - ) - if err != nil { - return nil, 0, err + readable, err = sstable.NewSimpleReadable(f) + if err != nil { + return nil, 0, err + } + } + // NB: ingestLoad1 will close readable. + meta[i], err = ingestLoad1(d.opts, d.FormatMajorVersion(), readable, d.cacheID, n) + if err != nil { + return nil, 0, errors.Wrap(err, "pebble: error when loading flushable ingest files") + } } if uint32(len(meta)) != b.Count() { @@ -854,9 +921,10 @@ func (d *DB) replayWAL( d.opts, d.mu.versions.currentVersion(), 1, /* base level */ []*flushableEntry{entry}, + d.timeNow(), ) for _, file := range c.flushing[0].flushable.(*ingestedFlushable).files { - ve.NewFiles = append(ve.NewFiles, newFileEntry{Level: 0, Meta: file}) + ve.NewFiles = append(ve.NewFiles, newFileEntry{Level: 0, Meta: file.FileMetadata}) } } return toFlush, maxSeqNum, nil @@ -868,11 +936,14 @@ func (d *DB) replayWAL( // Make a copy of the data slice since it is currently owned by buf and will // be reused in the next iteration. b.data = append([]byte(nil), b.data...) - b.flushable = newFlushableBatch(&b, d.opts.Comparer) + b.flushable, err = newFlushableBatch(&b, d.opts.Comparer) + if err != nil { + return nil, 0, err + } entry := d.newFlushableEntry(b.flushable, logNum, b.SeqNum()) // Disable memory accounting by adding a reader ref that will never be // removed. - entry.readerRefs++ + entry.readerRefs.Add(1) if d.opts.ReadOnly { d.mu.mem.queue = append(d.mu.mem.queue, entry) // We added the flushable batch to the flushable to the queue. @@ -910,7 +981,10 @@ func (d *DB) replayWAL( } buf.Reset() } + + d.opts.Logger.Infof("[JOB %d] WAL file %s with log number %s stopped reading at offset: %d; replayed %d keys in %d batches", jobID, filename, logNum.String(), offset, keysReplayed, batchesReplayed) flushMem() + // mem is nil here. if !d.opts.ReadOnly { err = updateVE() @@ -990,7 +1064,7 @@ func Peek(dirname string, fs vfs.FS) (*DBDesc, error) { // LockDirectory may be used to expand the critical section protected by the // database lock to include setup before the call to Open. func LockDirectory(dirname string, fs vfs.FS) (*Lock, error) { - fileLock, err := fs.Lock(base.MakeFilepath(fs, dirname, fileTypeLock, 0)) + fileLock, err := fs.Lock(base.MakeFilepath(fs, dirname, fileTypeLock, base.FileNum(0).DiskFileNum())) if err != nil { return nil, err } @@ -1068,28 +1142,42 @@ func IsCorruptionError(err error) bool { return errors.Is(err, base.ErrCorruption) } -func checkConsistency(v *manifest.Version, dirname string, objProvider *objstorage.Provider) error { +func checkConsistency(v *manifest.Version, dirname string, objProvider objstorage.Provider) error { var buf bytes.Buffer var args []interface{} + dedup := make(map[base.DiskFileNum]struct{}) for level, files := range v.Levels { iter := files.Iter() for f := iter.First(); f != nil; f = iter.Next() { - meta, err := objProvider.Lookup(base.FileTypeTable, f.FileNum) + backingState := f.FileBacking + if _, ok := dedup[backingState.DiskFileNum]; ok { + continue + } + dedup[backingState.DiskFileNum] = struct{}{} + fileNum := backingState.DiskFileNum + fileSize := backingState.Size + // We allow foreign objects to have a mismatch between sizes. This is + // because we might skew the backing size stored by our objprovider + // to prevent us from over-prioritizing this file for compaction. + meta, err := objProvider.Lookup(base.FileTypeTable, fileNum) var size int64 if err == nil { + if objProvider.IsSharedForeign(meta) { + continue + } size, err = objProvider.Size(meta) } if err != nil { buf.WriteString("L%d: %s: %v\n") - args = append(args, errors.Safe(level), errors.Safe(f.FileNum), err) + args = append(args, errors.Safe(level), errors.Safe(fileNum), err) continue } - if size != int64(f.Size) { + if size != int64(fileSize) { buf.WriteString("L%d: %s: object size mismatch (%s): %d (disk) != %d (MANIFEST)\n") - args = append(args, errors.Safe(level), errors.Safe(f.FileNum), objProvider.Path(meta), - errors.Safe(size), errors.Safe(f.Size)) + args = append(args, errors.Safe(level), errors.Safe(fileNum), objProvider.Path(meta), + errors.Safe(size), errors.Safe(fileSize)) continue } } diff --git a/vendor/github.com/cockroachdb/pebble/options.go b/vendor/github.com/cockroachdb/pebble/options.go index 895276c..f777daa 100644 --- a/vendor/github.com/cockroachdb/pebble/options.go +++ b/vendor/github.com/cockroachdb/pebble/options.go @@ -17,8 +17,10 @@ import ( "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/humanize" + "github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/manifest" - "github.com/cockroachdb/pebble/objstorage/shared" + "github.com/cockroachdb/pebble/objstorage/remote" + "github.com/cockroachdb/pebble/rangekey" "github.com/cockroachdb/pebble/sstable" "github.com/cockroachdb/pebble/vfs" ) @@ -117,11 +119,24 @@ type IterOptions struct { // false to skip scanning. This function must be thread-safe since the same // function can be used by multiple iterators, if the iterator is cloned. TableFilter func(userProps map[string]string) bool + // SkipPoint may be used to skip over point keys that don't match an + // arbitrary predicate during iteration. If set, the Iterator invokes + // SkipPoint for keys encountered. If SkipPoint returns true, the iterator + // will skip the key without yielding it to the iterator operation in + // progress. + // + // SkipPoint must be a pure function and always return the same result when + // provided the same arguments. The iterator may call SkipPoint multiple + // times for the same user key. + SkipPoint func(userKey []byte) bool // PointKeyFilters can be used to avoid scanning tables and blocks in tables - // when iterating over point keys. It is requires that this slice is sorted in - // increasing order of the BlockPropertyFilter.ShortID. This slice represents - // an intersection across all filters, i.e., all filters must indicate that the - // block is relevant. + // when iterating over point keys. This slice represents an intersection + // across all filters, i.e., all filters must indicate that the block is + // relevant. + // + // Performance note: When len(PointKeyFilters) > 0, the caller should ensure + // that cap(PointKeyFilters) is at least len(PointKeyFilters)+1. This helps + // avoid allocations in Pebble internal code that mutates the slice. PointKeyFilters []BlockPropertyFilter // RangeKeyFilters can be usefd to avoid scanning tables and blocks in tables // when iterating over range keys. The same requirements that apply to @@ -180,6 +195,10 @@ type IterOptions struct { level manifest.Level // disableLazyCombinedIteration is an internal testing option. disableLazyCombinedIteration bool + // snapshotForHideObsoletePoints is specified for/by levelIter when opening + // files and is used to decide whether to hide obsolete points. A value of 0 + // implies obsolete points should not be hidden. + snapshotForHideObsoletePoints uint64 // NB: If adding new Options, you must account for them in iterator // construction and Iterator.SetOptions. @@ -222,6 +241,38 @@ func (o *IterOptions) getLogger() Logger { return o.logger } +// SpanIterOptions creates a SpanIterOptions from this IterOptions. +func (o *IterOptions) SpanIterOptions() keyspan.SpanIterOptions { + if o == nil { + return keyspan.SpanIterOptions{} + } + return keyspan.SpanIterOptions{ + RangeKeyFilters: o.RangeKeyFilters, + } +} + +// scanInternalOptions is similar to IterOptions, meant for use with +// scanInternalIterator. +type scanInternalOptions struct { + IterOptions + + visitPointKey func(key *InternalKey, value LazyValue, iterInfo IteratorLevel) error + visitRangeDel func(start, end []byte, seqNum uint64) error + visitRangeKey func(start, end []byte, keys []rangekey.Key) error + visitSharedFile func(sst *SharedSSTMeta) error + + // skipSharedLevels skips levels that are shareable (level >= + // sharedLevelStart). + skipSharedLevels bool + + // includeObsoleteKeys specifies whether keys shadowed by newer internal keys + // are exposed. If false, only one internal key per user key is exposed. + includeObsoleteKeys bool + + // rateLimitFunc is used to limit the amount of bytes read per second. + rateLimitFunc func(key *InternalKey, value LazyValue) error +} + // RangeKeyMasking configures automatic hiding of point keys by range keys. A // non-nil Suffix enables range-key masking. When enabled, range keys with // suffixes ≥ Suffix behave as masks. All point keys that are contained within a @@ -497,18 +548,12 @@ type Options struct { // concurrent compaction is added. This works "on top" of // L0CompactionConcurrency, so the higher of the count of compaction // concurrency slots as determined by the two options is chosen. - CompactionDebtConcurrency int + CompactionDebtConcurrency uint64 - // MinDeletionRate is the minimum number of bytes per second that would - // be deleted. Deletion pacing is used to slow down deletions when - // compactions finish up or readers close, and newly-obsolete files need - // cleaning up. Deleting lots of files at once can cause disk latency to - // go up on some SSDs, which this functionality guards against. This is a - // minimum as the maximum is theoretically unlimited; pacing is disabled - // when there are too many obsolete files relative to live bytes, or - // there isn't enough disk space available. Setting this to 0 disables - // deletion pacing, which is also the default. - MinDeletionRate int + // IngestSplit, if it returns true, allows for ingest-time splitting of + // existing sstables into two virtual sstables to allow ingestion sstables to + // slot into a lower level than they otherwise would have. + IngestSplit func() bool // ReadCompactionRate controls the frequency of read triggered // compactions by adjusting `AllowedSeeks` in manifest.FileMetadata: @@ -573,10 +618,10 @@ type Options struct { // desired size of each level of the LSM. Defaults to 10. LevelMultiplier int - // MultiLevelCompactionHueristic determines whether to add an additional + // MultiLevelCompactionHeuristic determines whether to add an additional // level to a conventional two level compaction. If nil, a multilevel // compaction will never get triggered. - MultiLevelCompactionHueristic MultiLevelHeuristic + MultiLevelCompactionHeuristic MultiLevelHeuristic // MaxWriterConcurrency is used to indicate the maximum number of // compression workers the compression queue is allowed to use. If @@ -597,8 +642,11 @@ type Options struct { CPUWorkPermissionGranter CPUWorkPermissionGranter // EnableValueBlocks is used to decide whether to enable writing - // TableFormatPebblev3 sstables. WARNING: do not return true yet, since - // support for TableFormatPebblev3 is incomplete and not production ready. + // TableFormatPebblev3 sstables. This setting is only respected by a + // specific subset of format major versions: FormatSSTableValueBlocks, + // FormatFlushableIngest and FormatPrePebblev1MarkedCompacted. In lower + // format major versions, value blocks are never enabled. In higher + // format major versions, value blocks are always enabled. EnableValueBlocks func() bool // ShortAttributeExtractor is used iff EnableValueBlocks() returns true @@ -607,18 +655,24 @@ type Options struct { ShortAttributeExtractor ShortAttributeExtractor // RequiredInPlaceValueBound specifies an optional span of user key - // prefixes for which the values must be stored with the key. This is - // useful for statically known exclusions to value separation. In - // CockroachDB, this will be used for the lock table key space that has - // non-empty suffixes, but those locks don't represent actual MVCC - // versions (the suffix ordering is arbitrary). We will also need to add - // support for dynamically configured exclusions (we want the default to - // be to allow Pebble to decide whether to separate the value or not, - // hence this is structured as exclusions), for example, for users of - // CockroachDB to dynamically exclude certain tables. + // prefixes that are not-MVCC, but have a suffix. For these the values + // must be stored with the key, since the concept of "older versions" is + // not defined. It is also useful for statically known exclusions to value + // separation. In CockroachDB, this will be used for the lock table key + // space that has non-empty suffixes, but those locks don't represent + // actual MVCC versions (the suffix ordering is arbitrary). We will also + // need to add support for dynamically configured exclusions (we want the + // default to be to allow Pebble to decide whether to separate the value + // or not, hence this is structured as exclusions), for example, for users + // of CockroachDB to dynamically exclude certain tables. // // Any change in exclusion behavior takes effect only on future written // sstables, and does not start rewriting existing sstables. + // + // Even ignoring changes in this setting, exclusions are interpreted as a + // guidance by Pebble, and not necessarily honored. Specifically, user + // keys with multiple Pebble-versions *may* have the older versions stored + // in value blocks. RequiredInPlaceValueBound UserKeyPrefixBound // DisableIngestAsFlushable disables lazy ingestion of sstables through @@ -626,15 +680,49 @@ type Options struct { // major version is at least `FormatFlushableIngest`. DisableIngestAsFlushable func() bool - // SharedStorage is a second FS-like storage medium that can be shared - // between multiple Pebble instances. It is used to store sstables only, and - // is managed by objstorage.Provider. Each sstable might only be written to - // by one Pebble instance, but other Pebble instances can possibly read the - // same files if they have the path to get to them. The pebble instance that - // wrote a file should not delete it if other Pebble instances are known to - // be reading this file. This FS is expected to have slower read/write - // performance than the default FS above. - SharedStorage shared.Storage + // RemoteStorage enables use of remote storage (e.g. S3) for storing + // sstables. Setting this option enables use of CreateOnShared option and + // allows ingestion of external files. + RemoteStorage remote.StorageFactory + + // If CreateOnShared is non-zero, new sstables are created on remote storage + // (using CreateOnSharedLocator and with the appropriate + // CreateOnSharedStrategy). These sstables can be shared between different + // Pebble instances; the lifecycle of such objects is managed by the + // remote.Storage constructed by options.RemoteStorage. + // + // Can only be used when RemoteStorage is set (and recognizes + // CreateOnSharedLocator). + CreateOnShared remote.CreateOnSharedStrategy + CreateOnSharedLocator remote.Locator + + // CacheSizeBytesBytes is the size of the on-disk block cache for objects + // on shared storage in bytes. If it is 0, no cache is used. + SecondaryCacheSizeBytes int64 + + // IneffectualPointDeleteCallback is called in compactions/flushes if any + // single delete is being elided without deleting a point set/merge. + IneffectualSingleDeleteCallback func(userKey []byte) + + // SingleDeleteInvariantViolationCallback is called in compactions/flushes if any + // single delete has consumed a Set/Merge, and there is another immediately older + // Set/SetWithDelete/Merge. The user of Pebble has violated the invariant under + // which SingleDelete can be used correctly. + // + // Consider the sequence SingleDelete#3, Set#2, Set#1. There are three + // ways some of these keys can first meet in a compaction. + // + // - All 3 keys in the same compaction: this callback will detect the + // violation. + // + // - SingleDelete#3, Set#2 meet in a compaction first: Both keys will + // disappear. The violation will not be detected, and the DB will have + // Set#1 which is likely incorrect (from the user's perspective). + // + // - Set#2, Set#1 meet in a compaction first: The output will be Set#2, + // which will later be consumed by SingleDelete#3. The violation will + // not be detected and the DB will be correct. + SingleDeleteInvariantViolationCallback func(userKey []byte) } // Filters is a map from filter policy name to filter policy. It is used for @@ -749,12 +837,18 @@ type Options struct { // writing the contents of the old one in the // background. MemTableStopWritesThreshold places a hard limit on the size of // the queued MemTables. - MemTableSize int + // + // The default value is 4MB. + MemTableSize uint64 - // Hard limit on the size of queued of MemTables. Writes are stopped when the - // sum of the queued memtable sizes exceeds - // MemTableStopWritesThreshold*MemTableSize. This value should be at least 2 - // or writes will stop whenever a MemTable is being flushed. + // Hard limit on the number of queued of MemTables. Writes are stopped when + // the sum of the queued memtable sizes exceeds: + // MemTableStopWritesThreshold * MemTableSize. + // + // This value should be at least 2 or writes will stop whenever a MemTable is + // being flushed. + // + // The default value is 2. MemTableStopWritesThreshold int // Merger defines the associative merge operation to use for merging values @@ -839,6 +933,20 @@ type Options struct { // changing options dynamically? WALMinSyncInterval func() time.Duration + // TargetByteDeletionRate is the rate (in bytes per second) at which sstable file + // deletions are limited to (under normal circumstances). + // + // Deletion pacing is used to slow down deletions when compactions finish up + // or readers close and newly-obsolete files need cleaning up. Deleting lots + // of files at once can cause disk latency to go up on some SSDs, which this + // functionality guards against. + // + // This value is only a best-effort target; the effective rate can be + // higher if deletions are falling behind or disk space is running low. + // + // Setting this to 0 disables deletion pacing, which is also the default. + TargetByteDeletionRate int + // private options are only used by internal tests or are used internally // for facilitating upgrade paths of unconfigurable functionality. private struct { @@ -872,6 +980,10 @@ type Options struct { // A private option to disable stats collection. disableTableStats bool + // testingAlwaysWaitForCleanup is set by some tests to force waiting for + // obsolete file deletion (to make events deterministic). + testingAlwaysWaitForCleanup bool + // fsCloser holds a closer that should be invoked after a DB using these // Options is closed. This is used to automatically stop the // long-running goroutine associated with the disk-health-checking FS. @@ -983,7 +1095,7 @@ func (o *Options) EnsureDefaults() *Options { o.MaxOpenFiles = 1000 } if o.MemTableSize <= 0 { - o.MemTableSize = 4 << 20 + o.MemTableSize = 4 << 20 // 4 MB } if o.MemTableStopWritesThreshold <= 0 { o.MemTableStopWritesThreshold = 2 @@ -1024,8 +1136,8 @@ func (o *Options) EnsureDefaults() *Options { if o.Experimental.CPUWorkPermissionGranter == nil { o.Experimental.CPUWorkPermissionGranter = defaultCPUWorkGranter{} } - if o.Experimental.MultiLevelCompactionHueristic == nil { - o.Experimental.MultiLevelCompactionHueristic = NoMultiLevel{} + if o.Experimental.MultiLevelCompactionHeuristic == nil { + o.Experimental.MultiLevelCompactionHeuristic = WriteAmpHeuristic{} } o.initMaps() @@ -1144,7 +1256,7 @@ func (o *Options) String() string { fmt.Fprintf(&buf, " max_open_files=%d\n", o.MaxOpenFiles) fmt.Fprintf(&buf, " mem_table_size=%d\n", o.MemTableSize) fmt.Fprintf(&buf, " mem_table_stop_writes_threshold=%d\n", o.MemTableStopWritesThreshold) - fmt.Fprintf(&buf, " min_deletion_rate=%d\n", o.Experimental.MinDeletionRate) + fmt.Fprintf(&buf, " min_deletion_rate=%d\n", o.TargetByteDeletionRate) fmt.Fprintf(&buf, " merger=%s\n", o.Merger.Name) fmt.Fprintf(&buf, " read_compaction_rate=%d\n", o.Experimental.ReadCompactionRate) fmt.Fprintf(&buf, " read_sampling_multiplier=%d\n", o.Experimental.ReadSamplingMultiplier) @@ -1165,6 +1277,8 @@ func (o *Options) String() string { fmt.Fprintf(&buf, " wal_bytes_per_sync=%d\n", o.WALBytesPerSync) fmt.Fprintf(&buf, " max_writer_concurrency=%d\n", o.Experimental.MaxWriterConcurrency) fmt.Fprintf(&buf, " force_writer_parallelism=%t\n", o.Experimental.ForceWriterParallelism) + fmt.Fprintf(&buf, " secondary_cache_size_bytes=%d\n", o.Experimental.SecondaryCacheSizeBytes) + fmt.Fprintf(&buf, " create_on_shared=%d\n", o.Experimental.CreateOnShared) // Private options. // @@ -1322,7 +1436,7 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error { } } case "compaction_debt_concurrency": - o.Experimental.CompactionDebtConcurrency, err = strconv.Atoi(value) + o.Experimental.CompactionDebtConcurrency, err = strconv.ParseUint(value, 10, 64) case "delete_range_flush_delay": // NB: This is a deprecated serialization of the // `flush_delay_delete_range`. @@ -1354,7 +1468,7 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error { // version is valid right here. var v uint64 v, err = strconv.ParseUint(value, 10, 64) - if vers := FormatMajorVersion(v); vers > FormatNewest || vers == FormatDefault { + if vers := FormatMajorVersion(v); vers > internalFormatNewest || vers == FormatDefault { err = errors.Newf("unknown format major version %d", o.FormatMajorVersion) } if err == nil { @@ -1387,14 +1501,14 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error { case "max_open_files": o.MaxOpenFiles, err = strconv.Atoi(value) case "mem_table_size": - o.MemTableSize, err = strconv.Atoi(value) + o.MemTableSize, err = strconv.ParseUint(value, 10, 64) case "mem_table_stop_writes_threshold": o.MemTableStopWritesThreshold, err = strconv.Atoi(value) case "min_compaction_rate": // Do nothing; option existed in older versions of pebble, and // may be meaningful again eventually. case "min_deletion_rate": - o.Experimental.MinDeletionRate, err = strconv.Atoi(value) + o.TargetByteDeletionRate, err = strconv.Atoi(value) case "min_flush_rate": // Do nothing; option existed in older versions of pebble, and // may be meaningful again eventually. @@ -1438,6 +1552,12 @@ func (o *Options) Parse(s string, hooks *ParseHooks) error { o.Experimental.MaxWriterConcurrency, err = strconv.Atoi(value) case "force_writer_parallelism": o.Experimental.ForceWriterParallelism, err = strconv.ParseBool(value) + case "secondary_cache_size_bytes": + o.Experimental.SecondaryCacheSizeBytes, err = strconv.ParseInt(value, 10, 64) + case "create_on_shared": + var createOnSharedInt int64 + createOnSharedInt, err = strconv.ParseInt(value, 10, 64) + o.Experimental.CreateOnShared = remote.CreateOnSharedStrategy(createOnSharedInt) default: if hooks != nil && hooks.SkipUnknown != nil && hooks.SkipUnknown(section+"."+key, value) { return nil @@ -1568,15 +1688,15 @@ func (o *Options) Validate() error { } if uint64(o.MemTableSize) >= maxMemTableSize { fmt.Fprintf(&buf, "MemTableSize (%s) must be < %s\n", - humanize.Uint64(uint64(o.MemTableSize)), humanize.Uint64(maxMemTableSize)) + humanize.Bytes.Uint64(uint64(o.MemTableSize)), humanize.Bytes.Uint64(maxMemTableSize)) } if o.MemTableStopWritesThreshold < 2 { fmt.Fprintf(&buf, "MemTableStopWritesThreshold (%d) must be >= 2\n", o.MemTableStopWritesThreshold) } - if o.FormatMajorVersion > FormatNewest { + if o.FormatMajorVersion > internalFormatNewest { fmt.Fprintf(&buf, "FormatMajorVersion (%d) must be <= %d\n", - o.FormatMajorVersion, FormatNewest) + o.FormatMajorVersion, internalFormatNewest) } if o.TableCache != nil && o.Cache != o.TableCache.cache { fmt.Fprintf(&buf, "underlying cache in the TableCache and the Cache dont match\n") @@ -1596,6 +1716,7 @@ func (o *Options) MakeReaderOptions() sstable.ReaderOptions { readerOpts.Comparer = o.Comparer readerOpts.Filters = o.Filters if o.Merger != nil { + readerOpts.Merge = o.Merger.Merge readerOpts.MergerName = o.Merger.Name } readerOpts.LoggerAndTracer = o.LoggerAndTracer @@ -1620,6 +1741,9 @@ func (o *Options) MakeWriterOptions(level int, format sstable.TableFormat) sstab if format >= sstable.TableFormatPebblev3 { writerOpts.ShortAttributeExtractor = o.Experimental.ShortAttributeExtractor writerOpts.RequiredInPlaceValueBound = o.Experimental.RequiredInPlaceValueBound + if format >= sstable.TableFormatPebblev4 && level == numLevels-1 { + writerOpts.WritingToLowestLevel = true + } } levelOpts := o.Level(level) writerOpts.BlockRestartInterval = levelOpts.BlockRestartInterval diff --git a/vendor/github.com/cockroachdb/pebble/pacer.go b/vendor/github.com/cockroachdb/pebble/pacer.go index 7546e56..a959ff4 100644 --- a/vendor/github.com/cockroachdb/pebble/pacer.go +++ b/vendor/github.com/cockroachdb/pebble/pacer.go @@ -7,30 +7,11 @@ package pebble import ( "sync" "time" - - "github.com/cockroachdb/errors" - "github.com/cockroachdb/pebble/internal/rate" ) -var nilPacer = &noopPacer{} - -type limiter interface { - DelayN(now time.Time, n int) time.Duration - AllowN(now time.Time, n int) bool - Burst() int -} - -// pacer is the interface for flush and compaction rate limiters. The rate limiter -// is possible applied on each iteration step of a flush or compaction. This is to -// limit background IO usage so that it does not contend with foreground traffic. -type pacer interface { - maybeThrottle(bytesIterated uint64) error - - reportDeletion(bytesToDelete uint64) -} - // deletionPacerInfo contains any info from the db necessary to make deletion -// pacing decisions. +// pacing decisions (to limit background IO usage so that it does not contend +// with foreground traffic). type deletionPacerInfo struct { freeBytes uint64 obsoleteBytes uint64 @@ -43,12 +24,17 @@ type deletionPacerInfo struct { // negatively impacted if too many blocks are deleted very quickly, so this // mechanism helps mitigate that. type deletionPacer struct { - limiter limiter - freeSpaceThreshold uint64 - obsoleteBytesMaxRatio float64 - targetByteDeletionRate int64 + // If there are less than freeSpaceThreshold bytes of free space on + // disk, increase the pace of deletions such that we delete enough bytes to + // get back to the threshold within the freeSpaceTimeframe. + freeSpaceThreshold uint64 + freeSpaceTimeframe time.Duration - getInfo func() deletionPacerInfo + // If the ratio of obsolete bytes to live bytes is greater than + // obsoleteBytesMaxRatio, increase the pace of deletions such that we delete + // enough bytes to get back to the threshold within the obsoleteBytesTimeframe. + obsoleteBytesMaxRatio float64 + obsoleteBytesTimeframe time.Duration mu struct { sync.Mutex @@ -57,110 +43,93 @@ type deletionPacer struct { // deletion rate to match the pace of deletions. history history } + + targetByteDeletionRate int64 + + getInfo func() deletionPacerInfo } const deletePacerHistory = 5 * time.Minute // newDeletionPacer instantiates a new deletionPacer for use when deleting -// obsolete files. The limiter passed in must be a singleton shared across this -// pebble instance. +// obsolete files. +// +// targetByteDeletionRate is the rate (in bytes/sec) at which we want to +// normally limit deletes (when we are not falling behind or running out of +// space). A value of 0.0 disables pacing. func newDeletionPacer( - targetByteDeletionRate int64, getInfo func() deletionPacerInfo, + now time.Time, targetByteDeletionRate int64, getInfo func() deletionPacerInfo, ) *deletionPacer { d := &deletionPacer{ - limiter: rate.NewLimiter(rate.Limit(targetByteDeletionRate), int(targetByteDeletionRate)), - - // If there are less than freeSpaceThreshold bytes of free space on - // disk, do not pace deletions at all. freeSpaceThreshold: 16 << 30, // 16 GB - // If the ratio of obsolete bytes to live bytes is greater than - // obsoleteBytesMaxRatio, do not pace deletions at all. - obsoleteBytesMaxRatio: 0.20, + freeSpaceTimeframe: 10 * time.Second, + + obsoleteBytesMaxRatio: 0.20, + obsoleteBytesTimeframe: 5 * time.Minute, targetByteDeletionRate: targetByteDeletionRate, - - getInfo: getInfo, + getInfo: getInfo, } - d.mu.history.Init(time.Now(), deletePacerHistory) + d.mu.history.Init(now, deletePacerHistory) return d } -// limit applies rate limiting if the current free disk space is more than -// freeSpaceThreshold, and the ratio of obsolete to live bytes is less than -// obsoleteBytesMaxRatio. -func (p *deletionPacer) limit(amount uint64, info deletionPacerInfo) error { - obsoleteBytesRatio := float64(1.0) - if info.liveBytes > 0 { - obsoleteBytesRatio = float64(info.obsoleteBytes) / float64(info.liveBytes) - } - paceDeletions := info.freeBytes > p.freeSpaceThreshold && - obsoleteBytesRatio < p.obsoleteBytesMaxRatio - - p.mu.Lock() - if historyRate := p.mu.history.Sum(time.Now()) / int64(deletePacerHistory/time.Second); historyRate > p.targetByteDeletionRate { - // Deletions have been happening at a rate higher than the target rate; we - // want to speed up deletions so they match the historic rate. We do this by - // decreasing the amount accordingly. - amount = uint64(float64(p.targetByteDeletionRate) / float64(historyRate) * float64(amount)) - } - p.mu.Unlock() - - if paceDeletions { - burst := p.limiter.Burst() - for amount > uint64(burst) { - d := p.limiter.DelayN(time.Now(), burst) - if d == rate.InfDuration { - return errors.Errorf("pacing failed") - } - time.Sleep(d) - amount -= uint64(burst) - } - d := p.limiter.DelayN(time.Now(), int(amount)) - if d == rate.InfDuration { - return errors.Errorf("pacing failed") - } - time.Sleep(d) - } else { - burst := p.limiter.Burst() - for amount > uint64(burst) { - // AllowN will subtract burst if there are enough tokens available, - // else leave the tokens untouched. That is, we are making a - // best-effort to account for this activity in the limiter, but by - // ignoring the return value, we do the activity instantaneously - // anyway. - p.limiter.AllowN(time.Now(), burst) - amount -= uint64(burst) - } - p.limiter.AllowN(time.Now(), int(amount)) - } - return nil -} - -// maybeThrottle slows down a deletion of this file if it's faster than -// opts.Experimental.MinDeletionRate. -func (p *deletionPacer) maybeThrottle(bytesToDelete uint64) error { - return p.limit(bytesToDelete, p.getInfo()) -} - -// reportDeletion is used to report a deletion to the pacer. The pacer uses it +// ReportDeletion is used to report a deletion to the pacer. The pacer uses it // to keep track of the recent rate of deletions and potentially increase the // deletion rate accordingly. // -// reportDeletion is thread-safe. -func (p *deletionPacer) reportDeletion(bytesToDelete uint64) { +// ReportDeletion is thread-safe. +func (p *deletionPacer) ReportDeletion(now time.Time, bytesToDelete uint64) { p.mu.Lock() defer p.mu.Unlock() - p.mu.history.Add(time.Now(), int64(bytesToDelete)) + p.mu.history.Add(now, int64(bytesToDelete)) } -type noopPacer struct{} +// PacingDelay returns the recommended pacing wait time (in seconds) for +// deleting the given number of bytes. +// +// PacingDelay is thread-safe. +func (p *deletionPacer) PacingDelay(now time.Time, bytesToDelete uint64) (waitSeconds float64) { + if p.targetByteDeletionRate == 0 { + // Pacing disabled. + return 0.0 + } -func (p *noopPacer) maybeThrottle(_ uint64) error { - return nil + baseRate := float64(p.targetByteDeletionRate) + // If recent deletion rate is more than our target, use that so that we don't + // fall behind. + historicRate := func() float64 { + p.mu.Lock() + defer p.mu.Unlock() + return float64(p.mu.history.Sum(now)) / deletePacerHistory.Seconds() + }() + if historicRate > baseRate { + baseRate = historicRate + } + + // Apply heuristics to increase the deletion rate. + var extraRate float64 + info := p.getInfo() + if info.freeBytes <= p.freeSpaceThreshold { + // Increase the rate so that we can free up enough bytes within the timeframe. + extraRate = float64(p.freeSpaceThreshold-info.freeBytes) / p.freeSpaceTimeframe.Seconds() + } + if info.liveBytes == 0 { + // We don't know the obsolete bytes ratio. Disable pacing altogether. + return 0.0 + } + obsoleteBytesRatio := float64(info.obsoleteBytes) / float64(info.liveBytes) + if obsoleteBytesRatio >= p.obsoleteBytesMaxRatio { + // Increase the rate so that we can free up enough bytes within the timeframe. + r := (obsoleteBytesRatio - p.obsoleteBytesMaxRatio) * float64(info.liveBytes) / p.obsoleteBytesTimeframe.Seconds() + if extraRate < r { + extraRate = r + } + } + + return float64(bytesToDelete) / (baseRate + extraRate) } -func (p *noopPacer) reportDeletion(_ uint64) {} - // history is a helper used to keep track of the recent history of a set of // data points (in our case deleted bytes), at limited granularity. // Specifically, we split the desired timeframe into 100 "epochs" and all times diff --git a/vendor/github.com/cockroachdb/pebble/range_keys.go b/vendor/github.com/cockroachdb/pebble/range_keys.go index 30549d5..13b5822 100644 --- a/vendor/github.com/cockroachdb/pebble/range_keys.go +++ b/vendor/github.com/cockroachdb/pebble/range_keys.go @@ -17,7 +17,7 @@ import ( func (i *Iterator) constructRangeKeyIter() { i.rangeKey.rangeKeyIter = i.rangeKey.iterConfig.Init( &i.comparer, i.seqNum, i.opts.LowerBound, i.opts.UpperBound, - &i.hasPrefix, &i.prefixOrFullSeekKey, true /* onlySets */, &i.rangeKey.rangeKeyBuffers.internal) + &i.hasPrefix, &i.prefixOrFullSeekKey, false /* internalKeys */, &i.rangeKey.rangeKeyBuffers.internal) // If there's an indexed batch with range keys, include it. if i.batch != nil { @@ -36,19 +36,24 @@ func (i *Iterator) constructRangeKeyIter() { } // Next are the flushables: memtables and large batches. - for j := len(i.readState.memtables) - 1; j >= 0; j-- { - mem := i.readState.memtables[j] - // We only need to read from memtables which contain sequence numbers older - // than seqNum. - if logSeqNum := mem.logSeqNum; logSeqNum >= i.seqNum { - continue - } - if rki := mem.newRangeKeyIter(&i.opts); rki != nil { - i.rangeKey.iterConfig.AddLevel(rki) + if i.readState != nil { + for j := len(i.readState.memtables) - 1; j >= 0; j-- { + mem := i.readState.memtables[j] + // We only need to read from memtables which contain sequence numbers older + // than seqNum. + if logSeqNum := mem.logSeqNum; logSeqNum >= i.seqNum { + continue + } + if rki := mem.newRangeKeyIter(&i.opts); rki != nil { + i.rangeKey.iterConfig.AddLevel(rki) + } } } - current := i.readState.current + current := i.version + if current == nil { + current = i.readState.current + } // Next are the file levels: L0 sub-levels followed by lower levels. // // Add file-specific iterators for L0 files containing range keys. This is less @@ -63,8 +68,7 @@ func (i *Iterator) constructRangeKeyIter() { // around Key Trailer order. iter := current.RangeKeyLevels[0].Iter() for f := iter.Last(); f != nil; f = iter.Prev() { - spanIterOpts := &keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} - spanIter, err := i.newIterRangeKey(f, spanIterOpts) + spanIter, err := i.newIterRangeKey(f, i.opts.SpanIterOptions()) if err != nil { i.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err}) continue @@ -78,7 +82,7 @@ func (i *Iterator) constructRangeKeyIter() { continue } li := i.rangeKey.iterConfig.NewLevelIter() - spanIterOpts := keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters} + spanIterOpts := i.opts.SpanIterOptions() li.Init(spanIterOpts, i.cmp, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(), manifest.Level(level), manifest.KeyTypeRange) i.rangeKey.iterConfig.AddLevel(li) @@ -366,24 +370,24 @@ func (m *rangeKeyMasking) Intersects(prop []byte) (bool, error) { // KeyIsWithinLowerBound implements the limitedBlockPropertyFilter interface // defined in the sstable package. It's used to restrict the masking block // property filter to only applying within the bounds of the active range key. -func (m *rangeKeyMasking) KeyIsWithinLowerBound(ik *InternalKey) bool { +func (m *rangeKeyMasking) KeyIsWithinLowerBound(key []byte) bool { // Invariant: m.maskSpan != nil // - // The provided `ik` is an inclusive lower bound of the block we're + // The provided `key` is an inclusive lower bound of the block we're // considering skipping. - return m.cmp(m.maskSpan.Start, ik.UserKey) <= 0 + return m.cmp(m.maskSpan.Start, key) <= 0 } // KeyIsWithinUpperBound implements the limitedBlockPropertyFilter interface // defined in the sstable package. It's used to restrict the masking block // property filter to only applying within the bounds of the active range key. -func (m *rangeKeyMasking) KeyIsWithinUpperBound(ik *InternalKey) bool { +func (m *rangeKeyMasking) KeyIsWithinUpperBound(key []byte) bool { // Invariant: m.maskSpan != nil // - // The provided `ik` is an *inclusive* upper bound of the block we're + // The provided `key` is an *inclusive* upper bound of the block we're // considering skipping, so the range key's end must be strictly greater // than the block bound for the block to be within bounds. - return m.cmp(m.maskSpan.End, ik.UserKey) > 0 + return m.cmp(m.maskSpan.End, key) > 0 } // lazyCombinedIter implements the internalIterator interface, wrapping a @@ -512,7 +516,11 @@ func (i *lazyCombinedIter) initCombinedIteration( // Initialize the Iterator's interleaving iterator. i.parent.rangeKey.iiter.Init( &i.parent.comparer, i.parent.pointIter, i.parent.rangeKey.rangeKeyIter, - &i.parent.rangeKeyMasking, i.parent.opts.LowerBound, i.parent.opts.UpperBound) + keyspan.InterleavingIterOpts{ + Mask: &i.parent.rangeKeyMasking, + LowerBound: i.parent.opts.LowerBound, + UpperBound: i.parent.opts.UpperBound, + }) // Set the parent's primary iterator to point to the combined, interleaving // iterator that's now initialized with our current state. diff --git a/vendor/github.com/cockroachdb/pebble/rangekey/rangekey.go b/vendor/github.com/cockroachdb/pebble/rangekey/rangekey.go new file mode 100644 index 0000000..93e7fbe --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/rangekey/rangekey.go @@ -0,0 +1,33 @@ +// Copyright 2022 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 rangekey provides functionality for working with range keys. +package rangekey + +import ( + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/rangekey" + "github.com/cockroachdb/pebble/sstable" +) + +// Fragmenter exports the keyspan.Fragmenter type. +type Fragmenter = keyspan.Fragmenter + +// Key exports the keyspan.Key type. +type Key = keyspan.Key + +// Span exports the keyspan.Span type. +type Span = keyspan.Span + +// IsRangeKey returns if this InternalKey is a range key. Alias for +// rangekey.IsRangeKey. +func IsRangeKey(ik sstable.InternalKey) bool { + return rangekey.IsRangeKey(ik.Kind()) +} + +// Decode decodes an InternalKey into a keyspan.Span, if it is a range key. If +// keysDst is provided, keys will be appended to keysDst to reduce allocations. +func Decode(ik sstable.InternalKey, val []byte, keysDst []keyspan.Key) (Span, error) { + return rangekey.Decode(ik, val, keysDst) +} diff --git a/vendor/github.com/cockroachdb/pebble/read_state.go b/vendor/github.com/cockroachdb/pebble/read_state.go index a389817..d3a78ba 100644 --- a/vendor/github.com/cockroachdb/pebble/read_state.go +++ b/vendor/github.com/cockroachdb/pebble/read_state.go @@ -20,14 +20,14 @@ import "sync/atomic" // in Go. type readState struct { db *DB - refcnt int32 + refcnt atomic.Int32 current *version memtables flushableList } // ref adds a reference to the readState. func (s *readState) ref() { - atomic.AddInt32(&s.refcnt, 1) + s.refcnt.Add(1) } // unref removes a reference to the readState. If this was the last reference, @@ -35,7 +35,7 @@ func (s *readState) ref() { // is NOT held as version.unref() will acquire it. See unrefLocked() if DB.mu // is held by the caller. func (s *readState) unref() { - if atomic.AddInt32(&s.refcnt, -1) != 0 { + if s.refcnt.Add(-1) != 0 { return } s.current.Unref() @@ -50,10 +50,11 @@ func (s *readState) unref() { // unrefLocked removes a reference to the readState. If this was the last // reference, the reference the readState holds on the version is -// released. Requires DB.mu is held as version.unrefLocked() requires it. See -// unref() if DB.mu is NOT held by the caller. +// released. +// +// DB.mu must be held. See unref() if DB.mu is NOT held by the caller. func (s *readState) unrefLocked() { - if atomic.AddInt32(&s.refcnt, -1) != 0 { + if s.refcnt.Add(-1) != 0 { return } s.current.UnrefLocked() @@ -61,9 +62,8 @@ func (s *readState) unrefLocked() { mem.readerUnrefLocked(true) } - // NB: Unlike readState.unref(), we don't attempt to cleanup newly obsolete - // tables as unrefLocked() is only called during DB shutdown to release the - // current readState. + // In this code path, the caller is responsible for scheduling obsolete table + // deletion as necessary. } // loadReadState returns the current readState. The returned readState must be @@ -82,10 +82,10 @@ func (d *DB) loadReadState() *readState { func (d *DB) updateReadStateLocked(checker func(*DB) error) { s := &readState{ db: d, - refcnt: 1, current: d.mu.versions.currentVersion(), memtables: d.mu.mem.queue, } + s.refcnt.Store(1) s.current.Ref() for _, mem := range s.memtables { mem.readerRef() diff --git a/vendor/github.com/cockroachdb/pebble/record/log_writer.go b/vendor/github.com/cockroachdb/pebble/record/log_writer.go index 9d7ddfa..4aa5d2d 100644 --- a/vendor/github.com/cockroachdb/pebble/record/log_writer.go +++ b/vendor/github.com/cockroachdb/pebble/record/log_writer.go @@ -20,10 +20,11 @@ import ( ) var walSyncLabels = pprof.Labels("pebble", "wal-sync") +var errClosedWriter = errors.New("pebble/record: closed LogWriter") type block struct { // buf[:written] has already been filled with fragments. Updated atomically. - written int32 + written atomic.Int32 // buf[:flushed] has already been flushed to w. flushed int32 buf [blockSize]byte @@ -69,7 +70,7 @@ type syncQueue struct { // // The head index is stored in the most-significant bits so that we can // atomically add to it and the overflow is harmless. - headTail uint64 + headTail atomic.Uint64 // slots is a ring buffer of values stored in this queue. The size must be a // power of 2. A slot is in use until the tail index has moved beyond it. @@ -78,7 +79,7 @@ type syncQueue struct { // blocked is an atomic boolean which indicates whether syncing is currently // blocked or can proceed. It is used by the implementation of // min-sync-interval to block syncing until the min interval has passed. - blocked uint32 + blocked atomic.Bool } const dequeueBits = 32 @@ -91,7 +92,7 @@ func (q *syncQueue) unpack(ptrs uint64) (head, tail uint32) { } func (q *syncQueue) push(wg *sync.WaitGroup, err *error) { - ptrs := atomic.LoadUint64(&q.headTail) + ptrs := q.headTail.Load() head, tail := q.unpack(ptrs) if (tail+uint32(len(q.slots)))&(1< 0 || written > w.block.flushed || !f.syncQ.empty() { break } @@ -442,8 +439,7 @@ func (w *LogWriter) flushLoop(context.Context) { // Found work to do, so no longer idle. workStartTime := time.Now() idleDuration := workStartTime.Sub(idleStartTime) - pending = pending[:len(f.pending)] - copy(pending, f.pending) + pending = append(pending[:0], f.pending...) f.pending = f.pending[:0] f.metrics.PendingBufferLen.AddSample(int64(len(pending))) @@ -459,7 +455,7 @@ func (w *LogWriter) flushLoop(context.Context) { // be ordered after we get the list of sync waiters from syncQ in order to // prevent a race where a waiter adds itself to syncQ, but this thread // picks up the entry in syncQ and not the buffered data. - written := atomic.LoadInt32(&w.block.written) + written := w.block.written.Load() data := w.block.buf[w.block.flushed:written] w.block.flushed = written @@ -562,32 +558,22 @@ func (w *LogWriter) flushBlock(b *block) error { if _, err := w.w.Write(b.buf[b.flushed:]); err != nil { return err } - b.written = 0 + b.written.Store(0) b.flushed = 0 w.free.Lock() w.free.blocks = append(w.free.blocks, b) - w.free.cond.Signal() w.free.Unlock() return nil } // queueBlock queues the current block for writing to the underlying writer, // allocates a new block and reserves space for the next header. -func (w *LogWriter) queueBlock() (waitDuration time.Duration) { +func (w *LogWriter) queueBlock() { // Allocate a new block, blocking until one is available. We do this first // because w.block is protected by w.flusher.Mutex. w.free.Lock() if len(w.free.blocks) == 0 { - if w.free.allocated < cap(w.free.blocks) { - w.free.allocated++ - w.free.blocks = append(w.free.blocks, &block{}) - } else { - now := time.Now() - for len(w.free.blocks) == 0 { - w.free.cond.Wait() - } - waitDuration = time.Since(now) - } + w.free.blocks = append(w.free.blocks, blockPool.Get().(*block)) } nextBlock := w.free.blocks[len(w.free.blocks)-1] w.free.blocks = w.free.blocks[:len(w.free.blocks)-1] @@ -602,28 +588,6 @@ func (w *LogWriter) queueBlock() (waitDuration time.Duration) { f.Unlock() w.blockNum++ - return waitDuration -} - -// ReserveAllFreeBlocksForTesting is used to only for testing. -func (w *LogWriter) ReserveAllFreeBlocksForTesting() (releaseFunc func()) { - w.free.Lock() - defer w.free.Unlock() - free := w.free.blocks - w.free.blocks = nil - return func() { - w.free.Lock() - defer w.free.Unlock() - // It is possible that someone has pushed a free block and w.free.blocks - // is no longer nil. That is harmless. Also, the waiter loops on the - // condition len(w.free.blocks) == 0, so to actually unblock it we need to - // give it a free block. - if len(free) == 0 { - free = append(free, &block{}) - } - w.free.blocks = free - w.free.cond.Broadcast() - } } // Close flushes and syncs any unwritten data and closes the writer. @@ -658,6 +622,7 @@ func (w *LogWriter) Close() error { if f.fsyncLatency != nil { f.fsyncLatency.Observe(float64(syncLatency)) } + free := w.free.blocks f.Unlock() if w.c != nil { @@ -667,7 +632,14 @@ func (w *LogWriter) Close() error { return cerr } } - w.err = errors.New("pebble/record: closed LogWriter") + + for _, b := range free { + b.flushed = 0 + b.written.Store(0) + blockPool.Put(b) + } + + w.err = errClosedWriter return err } @@ -675,20 +647,20 @@ func (w *LogWriter) Close() error { // of the record. // External synchronisation provided by commitPipeline.mu. func (w *LogWriter) WriteRecord(p []byte) (int64, error) { - logSize, _, err := w.SyncRecord(p, nil, nil) + logSize, err := w.SyncRecord(p, nil, nil) return logSize, err } -// SyncRecord writes a complete record. If wg!= nil the record will be +// SyncRecord writes a complete record. If wg != nil the record will be // asynchronously persisted to the underlying writer and done will be called on // the wait group upon completion. Returns the offset just past the end of the // record. // External synchronisation provided by commitPipeline.mu. func (w *LogWriter) SyncRecord( p []byte, wg *sync.WaitGroup, err *error, -) (logSize int64, waitDuration time.Duration, err2 error) { +) (logSize int64, err2 error) { if w.err != nil { - return -1, 0, w.err + return -1, w.err } // The `i == 0` condition ensures we handle empty records. Such records can @@ -696,9 +668,7 @@ func (w *LogWriter) SyncRecord( // MANIFEST is currently written using Writer, it is good to support the same // semantics with LogWriter. for i := 0; i == 0 || len(p) > 0; i++ { - var wd time.Duration - p, wd = w.emitFragment(i, p) - waitDuration += wd + p = w.emitFragment(i, p) } if wg != nil { @@ -712,35 +682,35 @@ func (w *LogWriter) SyncRecord( f.ready.Signal() } - offset := w.blockNum*blockSize + int64(w.block.written) + offset := w.blockNum*blockSize + int64(w.block.written.Load()) // Note that we don't return w.err here as a concurrent call to Close would // race with our read. That's ok because the only error we could be seeing is // one to syncing for which the caller can receive notification of by passing // in a non-nil err argument. - return offset, waitDuration, nil + return offset, nil } // Size returns the current size of the file. // External synchronisation provided by commitPipeline.mu. func (w *LogWriter) Size() int64 { - return w.blockNum*blockSize + int64(w.block.written) + return w.blockNum*blockSize + int64(w.block.written.Load()) } func (w *LogWriter) emitEOFTrailer() { // Write a recyclable chunk header with a different log number. Readers // will treat the header as EOF when the log number does not match. b := w.block - i := b.written + i := b.written.Load() binary.LittleEndian.PutUint32(b.buf[i+0:i+4], 0) // CRC binary.LittleEndian.PutUint16(b.buf[i+4:i+6], 0) // Size b.buf[i+6] = recyclableFullChunkType binary.LittleEndian.PutUint32(b.buf[i+7:i+11], w.logNum+1) // Log number - atomic.StoreInt32(&b.written, i+int32(recyclableHeaderSize)) + b.written.Store(i + int32(recyclableHeaderSize)) } -func (w *LogWriter) emitFragment(n int, p []byte) (remainingP []byte, waitDuration time.Duration) { +func (w *LogWriter) emitFragment(n int, p []byte) (remainingP []byte) { b := w.block - i := b.written + i := b.written.Load() first := n == 0 last := blockSize-i-recyclableHeaderSize >= int32(len(p)) @@ -764,17 +734,17 @@ func (w *LogWriter) emitFragment(n int, p []byte) (remainingP []byte, waitDurati j := i + int32(recyclableHeaderSize+r) binary.LittleEndian.PutUint32(b.buf[i+0:i+4], crc.New(b.buf[i+6:j]).Value()) binary.LittleEndian.PutUint16(b.buf[i+4:i+6], uint16(r)) - atomic.StoreInt32(&b.written, j) + b.written.Store(j) - if blockSize-b.written < recyclableHeaderSize { + if blockSize-b.written.Load() < recyclableHeaderSize { // There is no room for another fragment in the block, so fill the // remaining bytes with zeros and queue the block for flushing. - for i := b.written; i < blockSize; i++ { + for i := b.written.Load(); i < blockSize; i++ { b.buf[i] = 0 } - waitDuration = w.queueBlock() + w.queueBlock() } - return p[r:], waitDuration + return p[r:] } // Metrics must be called after Close. The callee will no longer modify the diff --git a/vendor/github.com/cockroachdb/pebble/scan_internal.go b/vendor/github.com/cockroachdb/pebble/scan_internal.go new file mode 100644 index 0000000..08bebfc --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/scan_internal.go @@ -0,0 +1,1007 @@ +// Copyright 2023 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 ( + "context" + "fmt" + + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/remote" +) + +const ( + // In skip-shared iteration mode, keys in levels sharedLevelsStart and greater + // (i.e. lower in the LSM) are skipped. + sharedLevelsStart = remote.SharedLevelsStart +) + +// ErrInvalidSkipSharedIteration is returned by ScanInternal if it was called +// with a shared file visitor function, and a file in a shareable level (i.e. +// level >= sharedLevelsStart) was found to not be in shared storage according +// to objstorage.Provider, or not shareable for another reason such as for +// containing keys newer than the snapshot sequence number. +var ErrInvalidSkipSharedIteration = errors.New("pebble: cannot use skip-shared iteration due to non-shareable files in lower levels") + +// SharedSSTMeta represents an sstable on shared storage that can be ingested +// by another pebble instance. This struct must contain all fields that are +// required for a Pebble instance to ingest a foreign sstable on shared storage, +// including constructing any relevant objstorage.Provider / remoteobjcat.Catalog +// data structures, as well as creating virtual FileMetadatas. +// +// Note that the Pebble instance creating and returning a SharedSSTMeta might +// not be the one that created the underlying sstable on shared storage to begin +// with; it's possible for a Pebble instance to reshare an sstable that was +// shared to it. +type SharedSSTMeta struct { + // Backing is the shared object underlying this SST. Can be attached to an + // objstorage.Provider. + Backing objstorage.RemoteObjectBackingHandle + + // Smallest and Largest internal keys for the overall bounds. The kind and + // SeqNum of these will reflect what is physically present on the source Pebble + // instance's view of the sstable; it's up to the ingesting instance to set the + // sequence number in the trailer to match the read-time sequence numbers + // reserved for the level this SST is being ingested into. The Kind is expected + // to remain unchanged by the ingesting instance. + // + // Note that these bounds could be narrower than the bounds of the underlying + // sstable; ScanInternal is expected to truncate sstable bounds to the user key + // bounds passed into that method. + Smallest, Largest InternalKey + + // SmallestRangeKey and LargestRangeKey are internal keys that denote the + // range key bounds of this sstable. Must lie within [Smallest, Largest]. + SmallestRangeKey, LargestRangeKey InternalKey + + // SmallestPointKey and LargestPointKey are internal keys that denote the + // point key bounds of this sstable. Must lie within [Smallest, Largest]. + SmallestPointKey, LargestPointKey InternalKey + + // Level denotes the level at which this file was present at read time. + // For files visited by ScanInternal, this value will only be 5 or 6. + Level uint8 + + // Size contains an estimate of the size of this sstable. + Size uint64 + + // fileNum at time of creation in the creator instance. Only used for + // debugging/tests. + fileNum base.FileNum +} + +func (s *SharedSSTMeta) cloneFromFileMeta(f *fileMetadata) { + *s = SharedSSTMeta{ + Smallest: f.Smallest.Clone(), + Largest: f.Largest.Clone(), + SmallestRangeKey: f.SmallestRangeKey.Clone(), + LargestRangeKey: f.LargestRangeKey.Clone(), + SmallestPointKey: f.SmallestPointKey.Clone(), + LargestPointKey: f.LargestPointKey.Clone(), + Size: f.Size, + fileNum: f.FileNum, + } +} + +type sharedByLevel []SharedSSTMeta + +func (s sharedByLevel) Len() int { return len(s) } +func (s sharedByLevel) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s sharedByLevel) Less(i, j int) bool { return s[i].Level < s[j].Level } + +type pcIterPos int + +const ( + pcIterPosCur pcIterPos = iota + pcIterPosNext +) + +// pointCollapsingIterator is an internalIterator that collapses point keys and +// returns at most one point internal key for each user key. Merges and +// SingleDels are not supported and result in a panic if encountered. Point keys +// deleted by rangedels are considered shadowed and not exposed. +// +// Only used in ScanInternal to return at most one internal key per user key. +type pointCollapsingIterator struct { + iter keyspan.InterleavingIter + pos pcIterPos + comparer *base.Comparer + merge base.Merge + err error + seqNum uint64 + // The current position of `iter`. Always owned by the underlying iter. + iterKey *InternalKey + // The last saved key. findNextEntry and similar methods are expected to save + // the current value of iterKey to savedKey if they're iterating away from the + // current key but still need to retain it. See comments in findNextEntry on + // how this field is used. + // + // At the end of a positioning call: + // - if pos == pcIterPosNext, iterKey is pointing to the next user key owned + // by `iter` while savedKey is holding a copy to our current key. + // - If pos == pcIterPosCur, iterKey is pointing to an `iter`-owned current + // key, and savedKey is either undefined or pointing to a version of the + // current key owned by this iterator (i.e. backed by savedKeyBuf). + savedKey InternalKey + savedKeyBuf []byte + // Value at the current iterator position, at iterKey. + iterValue base.LazyValue + // If fixedSeqNum is non-zero, all emitted points are verified to have this + // fixed sequence number. + fixedSeqNum uint64 +} + +func (p *pointCollapsingIterator) Span() *keyspan.Span { + return p.iter.Span() +} + +// SeekPrefixGE implements the InternalIterator interface. +func (p *pointCollapsingIterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + p.resetKey() + p.iterKey, p.iterValue = p.iter.SeekPrefixGE(prefix, key, flags) + p.pos = pcIterPosCur + if p.iterKey == nil { + return nil, base.LazyValue{} + } + return p.findNextEntry() +} + +// SeekGE implements the InternalIterator interface. +func (p *pointCollapsingIterator) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + p.resetKey() + p.iterKey, p.iterValue = p.iter.SeekGE(key, flags) + p.pos = pcIterPosCur + if p.iterKey == nil { + return nil, base.LazyValue{} + } + return p.findNextEntry() +} + +// SeekLT implements the InternalIterator interface. +func (p *pointCollapsingIterator) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +func (p *pointCollapsingIterator) resetKey() { + p.savedKey.UserKey = p.savedKeyBuf[:0] + p.savedKey.Trailer = 0 + p.iterKey = nil + p.pos = pcIterPosCur +} + +func (p *pointCollapsingIterator) verifySeqNum(key *base.InternalKey) *base.InternalKey { + if !invariants.Enabled { + return key + } + if p.fixedSeqNum == 0 || key == nil || key.Kind() == InternalKeyKindRangeDelete { + return key + } + if key.SeqNum() != p.fixedSeqNum { + panic(fmt.Sprintf("expected foreign point key to have seqnum %d, got %d", p.fixedSeqNum, key.SeqNum())) + } + return key +} + +// findNextEntry is called to return the next key. p.iter must be positioned at the +// start of the first user key we are interested in. +func (p *pointCollapsingIterator) findNextEntry() (*base.InternalKey, base.LazyValue) { + p.saveKey() + // Saves a comparison in the fast path + firstIteration := true + for p.iterKey != nil { + // NB: p.savedKey is either the current key (iff p.iterKey == firstKey), + // or the previous key. + if !firstIteration && !p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) { + p.saveKey() + continue + } + firstIteration = false + if s := p.iter.Span(); s != nil && s.CoversAt(p.seqNum, p.iterKey.SeqNum()) { + // All future keys for this user key must be deleted. + if p.savedKey.Kind() == InternalKeyKindSingleDelete { + panic("cannot process singledel key in point collapsing iterator") + } + // Fast forward to the next user key. + p.saveKey() + p.iterKey, p.iterValue = p.iter.Next() + for p.iterKey != nil && p.savedKey.SeqNum() >= p.iterKey.SeqNum() && p.comparer.Equal(p.iterKey.UserKey, p.savedKey.UserKey) { + p.iterKey, p.iterValue = p.iter.Next() + } + continue + } + switch p.savedKey.Kind() { + case InternalKeyKindSet, InternalKeyKindDelete, InternalKeyKindSetWithDelete, InternalKeyKindDeleteSized: + // Note that we return SETs directly, even if they would otherwise get + // compacted into a Del to turn into a SetWithDelete. This is a fast + // path optimization that can break SINGLEDEL determinism. To lead to + // consistent SINGLEDEL behaviour, this iterator should *not* be used for + // a keyspace where SINGLEDELs could be in use. If this iterator observes + // a SINGLEDEL as the first internal key for a user key, it will panic. + // + // As p.value is a lazy value owned by the child iterator, we can thread + // it through without loading it into p.valueBuf. + // + // TODO(bilal): We can even avoid saving the key in this fast path if + // we are in a block where setHasSamePrefix = false in a v3 sstable, + // guaranteeing that there's only one internal key for each user key. + // Thread this logic through the sstable iterators and/or consider + // collapsing (ha) this logic into the sstable iterators that are aware + // of blocks and can determine user key changes without doing key saves + // or comparisons. + p.pos = pcIterPosCur + return p.verifySeqNum(p.iterKey), p.iterValue + case InternalKeyKindSingleDelete: + // Panic, as this iterator is not expected to observe single deletes. + panic("cannot process singledel key in point collapsing iterator") + case InternalKeyKindMerge: + // Panic, as this iterator is not expected to observe merges. + panic("cannot process merge key in point collapsing iterator") + case InternalKeyKindRangeDelete: + // These are interleaved by the interleaving iterator ahead of all points. + // We should pass them as-is, but also account for any points ahead of + // them. + p.pos = pcIterPosCur + return p.verifySeqNum(p.iterKey), p.iterValue + default: + panic(fmt.Sprintf("unexpected kind: %d", p.iterKey.Kind())) + } + } + p.resetKey() + return nil, base.LazyValue{} +} + +// First implements the InternalIterator interface. +func (p *pointCollapsingIterator) First() (*base.InternalKey, base.LazyValue) { + p.resetKey() + p.iterKey, p.iterValue = p.iter.First() + p.pos = pcIterPosCur + if p.iterKey == nil { + return nil, base.LazyValue{} + } + return p.findNextEntry() +} + +// Last implements the InternalIterator interface. +func (p *pointCollapsingIterator) Last() (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +func (p *pointCollapsingIterator) saveKey() { + if p.iterKey == nil { + p.savedKey = InternalKey{UserKey: p.savedKeyBuf[:0]} + return + } + p.savedKeyBuf = append(p.savedKeyBuf[:0], p.iterKey.UserKey...) + p.savedKey = InternalKey{UserKey: p.savedKeyBuf, Trailer: p.iterKey.Trailer} +} + +// Next implements the InternalIterator interface. +func (p *pointCollapsingIterator) Next() (*base.InternalKey, base.LazyValue) { + switch p.pos { + case pcIterPosCur: + p.saveKey() + if p.iterKey != nil && p.iterKey.Kind() == InternalKeyKindRangeDelete { + // Step over the interleaved range delete and process the very next + // internal key, even if it's at the same user key. This is because a + // point for that user key has not been returned yet. + p.iterKey, p.iterValue = p.iter.Next() + break + } + // Fast forward to the next user key. + key, val := p.iter.Next() + // p.iterKey.SeqNum() >= key.SeqNum() is an optimization that allows us to + // use p.iterKey.SeqNum() < key.SeqNum() as a sign that the user key has + // changed, without needing to do the full key comparison. + for key != nil && p.savedKey.SeqNum() >= key.SeqNum() && + p.comparer.Equal(p.savedKey.UserKey, key.UserKey) { + key, val = p.iter.Next() + } + if key == nil { + // There are no keys to return. + p.resetKey() + return nil, base.LazyValue{} + } + p.iterKey, p.iterValue = key, val + case pcIterPosNext: + p.pos = pcIterPosCur + } + if p.iterKey == nil { + p.resetKey() + return nil, base.LazyValue{} + } + return p.findNextEntry() +} + +// NextPrefix implements the InternalIterator interface. +func (p *pointCollapsingIterator) NextPrefix(succKey []byte) (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// Prev implements the InternalIterator interface. +func (p *pointCollapsingIterator) Prev() (*base.InternalKey, base.LazyValue) { + panic("unimplemented") +} + +// Error implements the InternalIterator interface. +func (p *pointCollapsingIterator) Error() error { + if p.err != nil { + return p.err + } + return p.iter.Error() +} + +// Close implements the InternalIterator interface. +func (p *pointCollapsingIterator) Close() error { + return p.iter.Close() +} + +// SetBounds implements the InternalIterator interface. +func (p *pointCollapsingIterator) SetBounds(lower, upper []byte) { + p.resetKey() + p.iter.SetBounds(lower, upper) +} + +// String implements the InternalIterator interface. +func (p *pointCollapsingIterator) String() string { + return p.iter.String() +} + +var _ internalIterator = &pointCollapsingIterator{} + +// IteratorLevelKind is used to denote whether the current ScanInternal iterator +// is unknown, belongs to a flushable, or belongs to an LSM level type. +type IteratorLevelKind int8 + +const ( + // IteratorLevelUnknown indicates an unknown LSM level. + IteratorLevelUnknown IteratorLevelKind = iota + // IteratorLevelLSM indicates an LSM level. + IteratorLevelLSM + // IteratorLevelFlushable indicates a flushable (i.e. memtable). + IteratorLevelFlushable +) + +// IteratorLevel is used with scanInternalIterator to surface additional iterator-specific info where possible. +// Note: this is struct is only provided for point keys. +type IteratorLevel struct { + Kind IteratorLevelKind + // FlushableIndex indicates the position within the flushable queue of this level. + // Only valid if kind == IteratorLevelFlushable. + FlushableIndex int + // The level within the LSM. Only valid if Kind == IteratorLevelLSM. + Level int + // Sublevel is only valid if Kind == IteratorLevelLSM and Level == 0. + Sublevel int +} + +// scanInternalIterator is an iterator that returns all internal keys, including +// tombstones. For instance, an InternalKeyKindDelete would be returned as an +// InternalKeyKindDelete instead of the iterator skipping over to the next key. +// Internal keys within a user key are collapsed, eg. if there are two SETs, the +// one with the higher sequence is returned. Useful if an external user of Pebble +// needs to observe and rebuild Pebble's history of internal keys, such as in +// node-to-node replication. For use with {db,snapshot}.ScanInternal(). +// +// scanInternalIterator is expected to ignore point keys deleted by range +// deletions, and range keys shadowed by a range key unset or delete, however it +// *must* return the range delete as well as the range key unset/delete that did +// the shadowing. +type scanInternalIterator struct { + db *DB + opts scanInternalOptions + comparer *base.Comparer + merge Merge + iter internalIterator + readState *readState + version *version + rangeKey *iteratorRangeKeyState + pointKeyIter internalIterator + iterKey *InternalKey + iterValue LazyValue + alloc *iterAlloc + newIters tableNewIters + newIterRangeKey keyspan.TableNewSpanIter + seqNum uint64 + iterLevels []IteratorLevel + mergingIter *mergingIter + + // boundsBuf holds two buffers used to store the lower and upper bounds. + // Whenever the InternalIterator's bounds change, the new bounds are copied + // into boundsBuf[boundsBufIdx]. The two bounds share a slice to reduce + // allocations. opts.LowerBound and opts.UpperBound point into this slice. + boundsBuf [2][]byte + boundsBufIdx int +} + +// truncateSharedFile truncates a shared file's [Smallest, Largest] fields to +// [lower, upper), potentially opening iterators on the file to find keys within +// the requested bounds. A SharedSSTMeta is produced that is suitable for +// external consumption by other Pebble instances. If shouldSkip is true, this +// file does not contain any keys in [lower, upper) and can be skipped. +// +// TODO(bilal): If opening iterators and doing reads in this method is too +// inefficient, consider producing non-tight file bounds instead. +func (d *DB) truncateSharedFile( + ctx context.Context, + lower, upper []byte, + level int, + file *fileMetadata, + objMeta objstorage.ObjectMetadata, +) (sst *SharedSSTMeta, shouldSkip bool, err error) { + cmp := d.cmp + sst = &SharedSSTMeta{} + sst.cloneFromFileMeta(file) + sst.Level = uint8(level) + sst.Backing, err = d.objProvider.RemoteObjectBacking(&objMeta) + if err != nil { + return nil, false, err + } + needsLowerTruncate := cmp(lower, file.Smallest.UserKey) > 0 + needsUpperTruncate := cmp(upper, file.Largest.UserKey) < 0 || (cmp(upper, file.Largest.UserKey) == 0 && !file.Largest.IsExclusiveSentinel()) + // Fast path: file is entirely within [lower, upper). + if !needsLowerTruncate && !needsUpperTruncate { + return sst, false, nil + } + + // We will need to truncate file bounds in at least one direction. Open all + // relevant iterators. + iter, rangeDelIter, err := d.newIters(ctx, file, &IterOptions{ + LowerBound: lower, + UpperBound: upper, + level: manifest.Level(level), + }, internalIterOpts{}) + if err != nil { + return nil, false, err + } + defer iter.Close() + if rangeDelIter != nil { + rangeDelIter = keyspan.Truncate( + cmp, rangeDelIter, lower, upper, nil, nil, + false, /* panicOnUpperTruncate */ + ) + defer rangeDelIter.Close() + } + rangeKeyIter, err := d.tableNewRangeKeyIter(file, keyspan.SpanIterOptions{}) + if err != nil { + return nil, false, err + } + if rangeKeyIter != nil { + rangeKeyIter = keyspan.Truncate( + cmp, rangeKeyIter, lower, upper, nil, nil, + false, /* panicOnUpperTruncate */ + ) + defer rangeKeyIter.Close() + } + // Check if we need to truncate on the left side. This means finding a new + // LargestPointKey and LargestRangeKey that is >= lower. + if needsLowerTruncate { + sst.SmallestPointKey.UserKey = sst.SmallestPointKey.UserKey[:0] + sst.SmallestPointKey.Trailer = 0 + key, _ := iter.SeekGE(lower, base.SeekGEFlagsNone) + foundPointKey := key != nil + if key != nil { + sst.SmallestPointKey.CopyFrom(*key) + } + if rangeDelIter != nil { + span := rangeDelIter.SeekGE(lower) + if span != nil && (len(sst.SmallestPointKey.UserKey) == 0 || base.InternalCompare(cmp, span.SmallestKey(), sst.SmallestPointKey) < 0) { + sst.SmallestPointKey.CopyFrom(span.SmallestKey()) + foundPointKey = true + } + } + if !foundPointKey { + // There are no point keys in the span we're interested in. + sst.SmallestPointKey = InternalKey{} + sst.LargestPointKey = InternalKey{} + } + sst.SmallestRangeKey.UserKey = sst.SmallestRangeKey.UserKey[:0] + sst.SmallestRangeKey.Trailer = 0 + if rangeKeyIter != nil { + span := rangeKeyIter.SeekGE(lower) + if span != nil { + sst.SmallestRangeKey.CopyFrom(span.SmallestKey()) + } else { + // There are no range keys in the span we're interested in. + sst.SmallestRangeKey = InternalKey{} + sst.LargestRangeKey = InternalKey{} + } + } + } + // Check if we need to truncate on the right side. This means finding a new + // LargestPointKey and LargestRangeKey that is < upper. + if needsUpperTruncate { + sst.LargestPointKey.UserKey = sst.LargestPointKey.UserKey[:0] + sst.LargestPointKey.Trailer = 0 + key, _ := iter.SeekLT(upper, base.SeekLTFlagsNone) + foundPointKey := key != nil + if key != nil { + sst.LargestPointKey.CopyFrom(*key) + } + if rangeDelIter != nil { + span := rangeDelIter.SeekLT(upper) + if span != nil && (len(sst.LargestPointKey.UserKey) == 0 || base.InternalCompare(cmp, span.LargestKey(), sst.LargestPointKey) > 0) { + sst.LargestPointKey.CopyFrom(span.LargestKey()) + foundPointKey = true + } + } + if !foundPointKey { + // There are no point keys in the span we're interested in. + sst.SmallestPointKey = InternalKey{} + sst.LargestPointKey = InternalKey{} + } + sst.LargestRangeKey.UserKey = sst.LargestRangeKey.UserKey[:0] + sst.LargestRangeKey.Trailer = 0 + if rangeKeyIter != nil { + span := rangeKeyIter.SeekLT(upper) + if span != nil { + sst.LargestRangeKey.CopyFrom(span.LargestKey()) + } else { + // There are no range keys in the span we're interested in. + sst.SmallestRangeKey = InternalKey{} + sst.LargestRangeKey = InternalKey{} + } + } + } + // Set overall bounds based on {Smallest,Largest}{Point,Range}Key. + switch { + case len(sst.SmallestRangeKey.UserKey) == 0: + sst.Smallest = sst.SmallestPointKey + case len(sst.SmallestPointKey.UserKey) == 0: + sst.Smallest = sst.SmallestRangeKey + default: + sst.Smallest = sst.SmallestPointKey + if base.InternalCompare(cmp, sst.SmallestRangeKey, sst.SmallestPointKey) < 0 { + sst.Smallest = sst.SmallestRangeKey + } + } + switch { + case len(sst.LargestRangeKey.UserKey) == 0: + sst.Largest = sst.LargestPointKey + case len(sst.LargestPointKey.UserKey) == 0: + sst.Largest = sst.LargestRangeKey + default: + sst.Largest = sst.LargestPointKey + if base.InternalCompare(cmp, sst.LargestRangeKey, sst.LargestPointKey) > 0 { + sst.Largest = sst.LargestRangeKey + } + } + // On rare occasion, a file might overlap with [lower, upper) but not actually + // have any keys within those bounds. Skip such files. + if len(sst.Smallest.UserKey) == 0 { + return nil, true, nil + } + sst.Size, err = d.tableCache.estimateSize(file, sst.Smallest.UserKey, sst.Largest.UserKey) + if err != nil { + return nil, false, err + } + // On occasion, estimateSize gives us a low estimate, i.e. a 0 file size. This + // can cause panics in places where we divide by file sizes. Correct for it + // here. + if sst.Size == 0 { + sst.Size = 1 + } + return sst, false, nil +} + +func scanInternalImpl( + ctx context.Context, lower, upper []byte, iter *scanInternalIterator, opts *scanInternalOptions, +) error { + if opts.visitSharedFile != nil && (lower == nil || upper == nil) { + panic("lower and upper bounds must be specified in skip-shared iteration mode") + } + // Before starting iteration, check if any files in levels sharedLevelsStart + // and below are *not* shared. Error out if that is the case, as skip-shared + // iteration will not produce a consistent point-in-time view of this range + // of keys. For files that are shared, call visitSharedFile with a truncated + // version of that file. + cmp := iter.comparer.Compare + provider := iter.db.ObjProvider() + seqNum := iter.seqNum + current := iter.version + if current == nil { + current = iter.readState.current + } + if opts.visitSharedFile != nil { + if provider == nil { + panic("expected non-nil Provider in skip-shared iteration mode") + } + for level := sharedLevelsStart; level < numLevels; level++ { + files := current.Levels[level].Iter() + for f := files.SeekGE(cmp, lower); f != nil && cmp(f.Smallest.UserKey, upper) < 0; f = files.Next() { + var objMeta objstorage.ObjectMetadata + var err error + objMeta, err = provider.Lookup(fileTypeTable, f.FileBacking.DiskFileNum) + if err != nil { + return err + } + if !objMeta.IsShared() { + return errors.Wrapf(ErrInvalidSkipSharedIteration, "file %s is not shared", objMeta.DiskFileNum) + } + if !base.Visible(f.LargestSeqNum, seqNum, base.InternalKeySeqNumMax) { + return errors.Wrapf(ErrInvalidSkipSharedIteration, "file %s contains keys newer than snapshot", objMeta.DiskFileNum) + } + var sst *SharedSSTMeta + var skip bool + sst, skip, err = iter.db.truncateSharedFile(ctx, lower, upper, level, f, objMeta) + if err != nil { + return err + } + if skip { + continue + } + if err = opts.visitSharedFile(sst); err != nil { + return err + } + } + } + } + + for valid := iter.seekGE(lower); valid && iter.error() == nil; valid = iter.next() { + key := iter.unsafeKey() + + if opts.rateLimitFunc != nil { + if err := opts.rateLimitFunc(key, iter.lazyValue()); err != nil { + return err + } + } + + switch key.Kind() { + case InternalKeyKindRangeKeyDelete, InternalKeyKindRangeKeyUnset, InternalKeyKindRangeKeySet: + if opts.visitRangeKey != nil { + span := iter.unsafeSpan() + // NB: The caller isn't interested in the sequence numbers of these + // range keys. Rather, the caller wants them to be in trailer order + // _after_ zeroing of sequence numbers. Copy span.Keys, sort it, and then + // call visitRangeKey. + keysCopy := make([]keyspan.Key, len(span.Keys)) + for i := range span.Keys { + keysCopy[i] = span.Keys[i] + keysCopy[i].Trailer = base.MakeTrailer(0, span.Keys[i].Kind()) + } + keyspan.SortKeysByTrailer(&keysCopy) + if err := opts.visitRangeKey(span.Start, span.End, keysCopy); err != nil { + return err + } + } + case InternalKeyKindRangeDelete: + if opts.visitRangeDel != nil { + rangeDel := iter.unsafeRangeDel() + if err := opts.visitRangeDel(rangeDel.Start, rangeDel.End, rangeDel.LargestSeqNum()); err != nil { + return err + } + } + default: + if opts.visitPointKey != nil { + var info IteratorLevel + if len(iter.mergingIter.heap.items) > 0 { + mergingIterIdx := iter.mergingIter.heap.items[0].index + info = iter.iterLevels[mergingIterIdx] + } else { + info = IteratorLevel{Kind: IteratorLevelUnknown} + } + val := iter.lazyValue() + if err := opts.visitPointKey(key, val, info); err != nil { + return err + } + } + } + } + + return nil +} + +// constructPointIter constructs a merging iterator and sets i.iter to it. +func (i *scanInternalIterator) constructPointIter(memtables flushableList, buf *iterAlloc) { + // Merging levels and levels from iterAlloc. + mlevels := buf.mlevels[:0] + levels := buf.levels[:0] + + // We compute the number of levels needed ahead of time and reallocate a slice if + // the array from the iterAlloc isn't large enough. Doing this allocation once + // should improve the performance. + numMergingLevels := len(memtables) + numLevelIters := 0 + + current := i.version + if current == nil { + current = i.readState.current + } + numMergingLevels += len(current.L0SublevelFiles) + numLevelIters += len(current.L0SublevelFiles) + + for level := 1; level < len(current.Levels); level++ { + if current.Levels[level].Empty() { + continue + } + if i.opts.skipSharedLevels && level >= sharedLevelsStart { + continue + } + numMergingLevels++ + numLevelIters++ + } + + if numMergingLevels > cap(mlevels) { + mlevels = make([]mergingIterLevel, 0, numMergingLevels) + } + if numLevelIters > cap(levels) { + levels = make([]levelIter, 0, numLevelIters) + } + // TODO(bilal): Push these into the iterAlloc buf. + var rangeDelMiter keyspan.MergingIter + rangeDelIters := make([]keyspan.FragmentIterator, 0, numMergingLevels) + rangeDelLevels := make([]keyspan.LevelIter, 0, numLevelIters) + + i.iterLevels = make([]IteratorLevel, numMergingLevels) + mlevelsIndex := 0 + + // Next are the memtables. + for j := len(memtables) - 1; j >= 0; j-- { + mem := memtables[j] + mlevels = append(mlevels, mergingIterLevel{ + iter: mem.newIter(&i.opts.IterOptions), + }) + i.iterLevels[mlevelsIndex] = IteratorLevel{ + Kind: IteratorLevelFlushable, + FlushableIndex: j, + } + mlevelsIndex++ + if rdi := mem.newRangeDelIter(&i.opts.IterOptions); rdi != nil { + rangeDelIters = append(rangeDelIters, rdi) + } + } + + // Next are the file levels: L0 sub-levels followed by lower levels. + levelsIndex := len(levels) + mlevels = mlevels[:numMergingLevels] + levels = levels[:numLevelIters] + rangeDelLevels = rangeDelLevels[:numLevelIters] + i.opts.IterOptions.snapshotForHideObsoletePoints = i.seqNum + addLevelIterForFiles := func(files manifest.LevelIterator, level manifest.Level) { + li := &levels[levelsIndex] + rli := &rangeDelLevels[levelsIndex] + + li.init( + context.Background(), i.opts.IterOptions, i.comparer, i.newIters, files, level, + internalIterOpts{}) + li.initBoundaryContext(&mlevels[mlevelsIndex].levelIterBoundaryContext) + mlevels[mlevelsIndex].iter = li + rli.Init(keyspan.SpanIterOptions{RangeKeyFilters: i.opts.RangeKeyFilters}, + i.comparer.Compare, tableNewRangeDelIter(context.Background(), i.newIters), files, level, + manifest.KeyTypePoint) + rangeDelIters = append(rangeDelIters, rli) + + levelsIndex++ + mlevelsIndex++ + } + + for j := len(current.L0SublevelFiles) - 1; j >= 0; j-- { + i.iterLevels[mlevelsIndex] = IteratorLevel{ + Kind: IteratorLevelLSM, + Level: 0, + Sublevel: j, + } + addLevelIterForFiles(current.L0SublevelFiles[j].Iter(), manifest.L0Sublevel(j)) + } + // Add level iterators for the non-empty non-L0 levels. + for level := 1; level < numLevels; level++ { + if current.Levels[level].Empty() { + continue + } + if i.opts.skipSharedLevels && level >= sharedLevelsStart { + continue + } + i.iterLevels[mlevelsIndex] = IteratorLevel{Kind: IteratorLevelLSM, Level: level} + addLevelIterForFiles(current.Levels[level].Iter(), manifest.Level(level)) + } + + buf.merging.init(&i.opts.IterOptions, &InternalIteratorStats{}, i.comparer.Compare, i.comparer.Split, mlevels...) + buf.merging.snapshot = i.seqNum + rangeDelMiter.Init(i.comparer.Compare, keyspan.VisibleTransform(i.seqNum), new(keyspan.MergingBuffers), rangeDelIters...) + + if i.opts.includeObsoleteKeys { + iiter := &keyspan.InterleavingIter{} + iiter.Init(i.comparer, &buf.merging, &rangeDelMiter, + keyspan.InterleavingIterOpts{ + LowerBound: i.opts.LowerBound, + UpperBound: i.opts.UpperBound, + }) + i.pointKeyIter = iiter + } else { + pcIter := &pointCollapsingIterator{ + comparer: i.comparer, + merge: i.merge, + seqNum: i.seqNum, + } + pcIter.iter.Init(i.comparer, &buf.merging, &rangeDelMiter, keyspan.InterleavingIterOpts{ + LowerBound: i.opts.LowerBound, + UpperBound: i.opts.UpperBound, + }) + i.pointKeyIter = pcIter + } + i.iter = i.pointKeyIter +} + +// constructRangeKeyIter constructs the range-key iterator stack, populating +// i.rangeKey.rangeKeyIter with the resulting iterator. This is similar to +// Iterator.constructRangeKeyIter, except it doesn't handle batches and ensures +// iterConfig does *not* elide unsets/deletes. +func (i *scanInternalIterator) constructRangeKeyIter() { + // We want the bounded iter from iterConfig, but not the collapsing of + // RangeKeyUnsets and RangeKeyDels. + i.rangeKey.rangeKeyIter = i.rangeKey.iterConfig.Init( + i.comparer, i.seqNum, i.opts.LowerBound, i.opts.UpperBound, + nil /* hasPrefix */, nil /* prefix */, true, /* internalKeys */ + &i.rangeKey.rangeKeyBuffers.internal) + + // Next are the flushables: memtables and large batches. + if i.readState != nil { + for j := len(i.readState.memtables) - 1; j >= 0; j-- { + mem := i.readState.memtables[j] + // We only need to read from memtables which contain sequence numbers older + // than seqNum. + if logSeqNum := mem.logSeqNum; logSeqNum >= i.seqNum { + continue + } + if rki := mem.newRangeKeyIter(&i.opts.IterOptions); rki != nil { + i.rangeKey.iterConfig.AddLevel(rki) + } + } + } + + current := i.version + if current == nil { + current = i.readState.current + } + // Next are the file levels: L0 sub-levels followed by lower levels. + // + // Add file-specific iterators for L0 files containing range keys. This is less + // efficient than using levelIters for sublevels of L0 files containing + // range keys, but range keys are expected to be sparse anyway, reducing the + // cost benefit of maintaining a separate L0Sublevels instance for range key + // files and then using it here. + // + // NB: We iterate L0's files in reverse order. They're sorted by + // LargestSeqNum ascending, and we need to add them to the merging iterator + // in LargestSeqNum descending to preserve the merging iterator's invariants + // around Key Trailer order. + iter := current.RangeKeyLevels[0].Iter() + for f := iter.Last(); f != nil; f = iter.Prev() { + spanIter, err := i.newIterRangeKey(f, i.opts.SpanIterOptions()) + if err != nil { + i.rangeKey.iterConfig.AddLevel(&errorKeyspanIter{err: err}) + continue + } + i.rangeKey.iterConfig.AddLevel(spanIter) + } + + // Add level iterators for the non-empty non-L0 levels. + for level := 1; level < len(current.RangeKeyLevels); level++ { + if current.RangeKeyLevels[level].Empty() { + continue + } + if i.opts.skipSharedLevels && level >= sharedLevelsStart { + continue + } + li := i.rangeKey.iterConfig.NewLevelIter() + spanIterOpts := i.opts.SpanIterOptions() + li.Init(spanIterOpts, i.comparer.Compare, i.newIterRangeKey, current.RangeKeyLevels[level].Iter(), + manifest.Level(level), manifest.KeyTypeRange) + i.rangeKey.iterConfig.AddLevel(li) + } +} + +// seekGE seeks this iterator to the first key that's greater than or equal +// to the specified user key. +func (i *scanInternalIterator) seekGE(key []byte) bool { + i.iterKey, i.iterValue = i.iter.SeekGE(key, base.SeekGEFlagsNone) + return i.iterKey != nil +} + +// unsafeKey returns the unsafe InternalKey at the current position. The value +// is nil if the iterator is invalid or exhausted. +func (i *scanInternalIterator) unsafeKey() *InternalKey { + return i.iterKey +} + +// lazyValue returns a value pointer to the value at the current iterator +// position. Behaviour undefined if unsafeKey() returns a Range key or Rangedel +// kind key. +func (i *scanInternalIterator) lazyValue() LazyValue { + return i.iterValue +} + +// unsafeRangeDel returns a range key span. Behaviour undefined if UnsafeKey returns +// a non-rangedel kind. +func (i *scanInternalIterator) unsafeRangeDel() *keyspan.Span { + type spanInternalIterator interface { + Span() *keyspan.Span + } + return i.pointKeyIter.(spanInternalIterator).Span() +} + +// unsafeSpan returns a range key span. Behaviour undefined if UnsafeKey returns +// a non-rangekey type. +func (i *scanInternalIterator) unsafeSpan() *keyspan.Span { + return i.rangeKey.iiter.Span() +} + +// next advances the iterator in the forward direction, and returns the +// iterator's new validity state. +func (i *scanInternalIterator) next() bool { + i.iterKey, i.iterValue = i.iter.Next() + return i.iterKey != nil +} + +// error returns an error from the internal iterator, if there's any. +func (i *scanInternalIterator) error() error { + return i.iter.Error() +} + +// close closes this iterator, and releases any pooled objects. +func (i *scanInternalIterator) close() error { + if err := i.iter.Close(); err != nil { + return err + } + if i.readState != nil { + i.readState.unref() + } + if i.version != nil { + i.version.Unref() + } + if i.rangeKey != nil { + i.rangeKey.PrepareForReuse() + *i.rangeKey = iteratorRangeKeyState{ + rangeKeyBuffers: i.rangeKey.rangeKeyBuffers, + } + iterRangeKeyStateAllocPool.Put(i.rangeKey) + i.rangeKey = nil + } + if alloc := i.alloc; alloc != nil { + for j := range i.boundsBuf { + if cap(i.boundsBuf[j]) >= maxKeyBufCacheSize { + alloc.boundsBuf[j] = nil + } else { + alloc.boundsBuf[j] = i.boundsBuf[j] + } + } + *alloc = iterAlloc{ + keyBuf: alloc.keyBuf[:0], + boundsBuf: alloc.boundsBuf, + prefixOrFullSeekKey: alloc.prefixOrFullSeekKey[:0], + } + iterAllocPool.Put(alloc) + i.alloc = nil + } + return nil +} + +func (i *scanInternalIterator) initializeBoundBufs(lower, upper []byte) { + buf := i.boundsBuf[i.boundsBufIdx][:0] + if lower != nil { + buf = append(buf, lower...) + i.opts.LowerBound = buf + } else { + i.opts.LowerBound = nil + } + if upper != nil { + buf = append(buf, upper...) + i.opts.UpperBound = buf[len(buf)-len(upper):] + } else { + i.opts.UpperBound = nil + } + i.boundsBuf[i.boundsBufIdx] = buf + i.boundsBufIdx = 1 - i.boundsBufIdx +} diff --git a/vendor/github.com/cockroachdb/pebble/snapshot.go b/vendor/github.com/cockroachdb/pebble/snapshot.go index afd5bda..d745e8a 100644 --- a/vendor/github.com/cockroachdb/pebble/snapshot.go +++ b/vendor/github.com/cockroachdb/pebble/snapshot.go @@ -8,16 +8,29 @@ import ( "context" "io" "math" + "sync" + "sync/atomic" + "time" - "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/rangekey" ) +// ErrSnapshotExcised is returned from WaitForFileOnlySnapshot if an excise +// overlapping with one of the EventuallyFileOnlySnapshot's KeyRanges gets +// applied before the transition of that EFOS to a file-only snapshot. +var ErrSnapshotExcised = errors.New("pebble: snapshot excised before conversion to file-only snapshot") + // Snapshot provides a read-only point-in-time view of the DB state. type Snapshot struct { // The db the snapshot was created from. db *DB seqNum uint64 + // Set if part of an EventuallyFileOnlySnapshot. + efos *EventuallyFileOnlySnapshot + // The list the snapshot is linked into. list *snapshotList @@ -44,17 +57,17 @@ func (s *Snapshot) Get(key []byte) ([]byte, io.Closer, error) { // NewIter returns an iterator that is unpositioned (Iterator.Valid() will // return false). The iterator can be positioned via a call to SeekGE, // SeekLT, First or Last. -func (s *Snapshot) NewIter(o *IterOptions) *Iterator { +func (s *Snapshot) NewIter(o *IterOptions) (*Iterator, error) { return s.NewIterWithContext(context.Background(), o) } // NewIterWithContext is like NewIter, and additionally accepts a context for // tracing. -func (s *Snapshot) NewIterWithContext(ctx context.Context, o *IterOptions) *Iterator { +func (s *Snapshot) NewIterWithContext(ctx context.Context, o *IterOptions) (*Iterator, error) { if s.db == nil { panic(ErrClosed) } - return s.db.newIter(ctx, nil /* batch */, s, o) + return s.db.newIter(ctx, nil /* batch */, snapshotIterOpts{seqNum: s.seqNum}, o), nil } // ScanInternal scans all internal keys within the specified bounds, truncating @@ -64,33 +77,38 @@ func (s *Snapshot) NewIterWithContext(ctx context.Context, o *IterOptions) *Iter // See comment on db.ScanInternal for the behaviour that can be expected of // point keys deleted by range dels and keys masked by range keys. func (s *Snapshot) ScanInternal( + ctx context.Context, lower, upper []byte, - visitPointKey func(key *InternalKey, value LazyValue) error, + visitPointKey func(key *InternalKey, value LazyValue, iterInfo IteratorLevel) error, visitRangeDel func(start, end []byte, seqNum uint64) error, - visitRangeKey func(start, end []byte, keys []keyspan.Key) error, + visitRangeKey func(start, end []byte, keys []rangekey.Key) error, + visitSharedFile func(sst *SharedSSTMeta) error, ) error { if s.db == nil { panic(ErrClosed) } - iter := s.db.newInternalIter(s, &IterOptions{ - KeyTypes: IterKeyTypePointsAndRanges, - LowerBound: lower, - UpperBound: upper, - }) + scanInternalOpts := &scanInternalOptions{ + visitPointKey: visitPointKey, + visitRangeDel: visitRangeDel, + visitRangeKey: visitRangeKey, + visitSharedFile: visitSharedFile, + skipSharedLevels: visitSharedFile != nil, + IterOptions: IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + LowerBound: lower, + UpperBound: upper, + }, + } + + iter := s.db.newInternalIter(snapshotIterOpts{seqNum: s.seqNum}, scanInternalOpts) defer iter.close() - return scanInternalImpl(lower, iter, visitPointKey, visitRangeDel, visitRangeKey) + return scanInternalImpl(ctx, lower, upper, iter, scanInternalOpts) } -// Close closes the snapshot, releasing its resources. Close must be called. -// Failure to do so will result in a tiny memory leak and a large leak of -// resources on disk due to the entries the snapshot is preventing from being -// deleted. -func (s *Snapshot) Close() error { - if s.db == nil { - panic(ErrClosed) - } - s.db.mu.Lock() +// closeLocked is similar to Close(), except it requires that db.mu be held +// by the caller. +func (s *Snapshot) closeLocked() error { s.db.mu.snapshots.remove(s) // If s was the previous earliest snapshot, we might be able to reclaim @@ -98,11 +116,26 @@ func (s *Snapshot) Close() error { if e := s.db.mu.snapshots.earliest(); e > s.seqNum { s.db.maybeScheduleCompactionPicker(pickElisionOnly) } - s.db.mu.Unlock() s.db = nil return nil } +// Close closes the snapshot, releasing its resources. Close must be called. +// Failure to do so will result in a tiny memory leak and a large leak of +// resources on disk due to the entries the snapshot is preventing from being +// deleted. +// +// d.mu must NOT be held by the caller. +func (s *Snapshot) Close() error { + db := s.db + if db == nil { + panic(ErrClosed) + } + db.mu.Lock() + defer db.mu.Unlock() + return s.closeLocked() +} + type snapshotList struct { root Snapshot } @@ -170,3 +203,345 @@ func (l *snapshotList) remove(s *Snapshot) { s.prev = nil // avoid memory leaks s.list = nil // avoid memory leaks } + +// EventuallyFileOnlySnapshot (aka EFOS) provides a read-only point-in-time view +// of the database state, similar to Snapshot. An EventuallyFileOnlySnapshot +// induces less write amplification than Snapshot, at the cost of increased space +// amplification. While a Snapshot may increase write amplification across all +// flushes and compactions for the duration of its lifetime, an +// EventuallyFileOnlySnapshot only incurs that cost for flushes/compactions if +// memtables at the time of EFOS instantiation contained keys that the EFOS is +// interested in (i.e. its protectedRanges). In that case, the EFOS prevents +// elision of keys visible to it, similar to a Snapshot, until those memtables +// are flushed, and once that happens, the "EventuallyFileOnlySnapshot" +// transitions to a file-only snapshot state in which it pins zombies sstables +// like an open Iterator would, without pinning any memtables. Callers that can +// tolerate the increased space amplification of pinning zombie sstables until +// the snapshot is closed may prefer EventuallyFileOnlySnapshots for their +// reduced write amplification. Callers that desire the benefits of the file-only +// state that requires no pinning of memtables should call +// `WaitForFileOnlySnapshot()` (and possibly re-mint an EFOS if it returns +// ErrSnapshotExcised) before relying on the EFOS to keep producing iterators +// with zero write-amp and zero pinning of memtables in memory. +// +// EventuallyFileOnlySnapshots interact with the IngestAndExcise operation in +// subtle ways. No new iterators can be created once +// EventuallyFileOnlySnapshot.excised is set to true. +type EventuallyFileOnlySnapshot struct { + mu struct { + // NB: If both this mutex and db.mu are being grabbed, db.mu should be + // grabbed _before_ grabbing this one. + sync.Mutex + + // Either the snap field is set below, or the version is set at any given + // point of time. If a snapshot is referenced, this is not a file-only + // snapshot yet, and if a version is set (and ref'd) this is a file-only + // snapshot. + + // The wrapped regular snapshot, if not a file-only snapshot yet. + snap *Snapshot + // The wrapped version reference, if a file-only snapshot. + vers *version + } + + // Key ranges to watch for an excise on. + protectedRanges []KeyRange + // excised, if true, signals that the above ranges were excised during the + // lifetime of this snapshot. + excised atomic.Bool + + // The db the snapshot was created from. + db *DB + seqNum uint64 + + closed chan struct{} +} + +func (d *DB) makeEventuallyFileOnlySnapshot( + keyRanges []KeyRange, internalKeyRanges []internalKeyRange, +) *EventuallyFileOnlySnapshot { + isFileOnly := true + + d.mu.Lock() + defer d.mu.Unlock() + seqNum := d.mu.versions.visibleSeqNum.Load() + // Check if any of the keyRanges overlap with a memtable. + for i := range d.mu.mem.queue { + mem := d.mu.mem.queue[i] + if ingestMemtableOverlaps(d.cmp, mem, internalKeyRanges) { + isFileOnly = false + break + } + } + es := &EventuallyFileOnlySnapshot{ + db: d, + seqNum: seqNum, + protectedRanges: keyRanges, + closed: make(chan struct{}), + } + if isFileOnly { + es.mu.vers = d.mu.versions.currentVersion() + es.mu.vers.Ref() + } else { + s := &Snapshot{ + db: d, + seqNum: seqNum, + } + s.efos = es + es.mu.snap = s + d.mu.snapshots.pushBack(s) + } + return es +} + +// Transitions this EventuallyFileOnlySnapshot to a file-only snapshot. Requires +// earliestUnflushedSeqNum and vers to correspond to the same Version from the +// current or a past acquisition of db.mu. vers must have been Ref()'d before +// that mutex was released, if it was released. +// +// NB: The caller is expected to check for es.excised before making this +// call. +// +// d.mu must be held when calling this method. +func (es *EventuallyFileOnlySnapshot) transitionToFileOnlySnapshot(vers *version) error { + es.mu.Lock() + select { + case <-es.closed: + vers.UnrefLocked() + es.mu.Unlock() + return ErrClosed + default: + } + if es.mu.snap == nil { + es.mu.Unlock() + panic("pebble: tried to transition an eventually-file-only-snapshot twice") + } + // The caller has already called Ref() on vers. + es.mu.vers = vers + // NB: The callers should have already done a check of es.excised. + oldSnap := es.mu.snap + es.mu.snap = nil + es.mu.Unlock() + return oldSnap.closeLocked() +} + +// hasTransitioned returns true if this EFOS has transitioned to a file-only +// snapshot. +func (es *EventuallyFileOnlySnapshot) hasTransitioned() bool { + es.mu.Lock() + defer es.mu.Unlock() + return es.mu.vers != nil +} + +// waitForFlush waits for a flush on any memtables that need to be flushed +// before this EFOS can transition to a file-only snapshot. If this EFOS is +// waiting on a flush of the mutable memtable, it forces a rotation within +// `dur` duration. For immutable memtables, it schedules a flush and waits for +// it to finish. +func (es *EventuallyFileOnlySnapshot) waitForFlush(ctx context.Context, dur time.Duration) error { + es.db.mu.Lock() + defer es.db.mu.Unlock() + + earliestUnflushedSeqNum := es.db.getEarliestUnflushedSeqNumLocked() + for earliestUnflushedSeqNum < es.seqNum { + select { + case <-es.closed: + return ErrClosed + case <-ctx.Done(): + return ctx.Err() + default: + } + // Check if the current mutable memtable contains keys less than seqNum. + // If so, rotate it. + if es.db.mu.mem.mutable.logSeqNum < es.seqNum && dur.Nanoseconds() > 0 { + es.db.maybeScheduleDelayedFlush(es.db.mu.mem.mutable, dur) + } else { + // Find the last memtable that contains seqNums less than es.seqNum, + // and force a flush on it. + var mem *flushableEntry + for i := range es.db.mu.mem.queue { + if es.db.mu.mem.queue[i].logSeqNum < es.seqNum { + mem = es.db.mu.mem.queue[i] + } + } + mem.flushForced = true + es.db.maybeScheduleFlush() + } + es.db.mu.compact.cond.Wait() + + earliestUnflushedSeqNum = es.db.getEarliestUnflushedSeqNumLocked() + } + if es.excised.Load() { + return ErrSnapshotExcised + } + return nil +} + +// WaitForFileOnlySnapshot blocks the calling goroutine until this snapshot +// has been converted into a file-only snapshot (i.e. all memtables containing +// keys < seqNum are flushed). A duration can be passed in, and if nonzero, +// a delayed flush will be scheduled at that duration if necessary. +// +// Idempotent; can be called multiple times with no side effects. +func (es *EventuallyFileOnlySnapshot) WaitForFileOnlySnapshot( + ctx context.Context, dur time.Duration, +) error { + if es.hasTransitioned() { + return nil + } + + if err := es.waitForFlush(ctx, dur); err != nil { + return err + } + + if invariants.Enabled { + // Since we aren't returning an error, we _must_ have transitioned to a + // file-only snapshot by now. + if !es.hasTransitioned() { + panic("expected EFOS to have transitioned to file-only snapshot after flush") + } + } + return nil +} + +// Close closes the file-only snapshot and releases all referenced resources. +// Not idempotent. +func (es *EventuallyFileOnlySnapshot) Close() error { + close(es.closed) + es.db.mu.Lock() + defer es.db.mu.Unlock() + es.mu.Lock() + defer es.mu.Unlock() + + if es.mu.snap != nil { + if err := es.mu.snap.closeLocked(); err != nil { + return err + } + } + if es.mu.vers != nil { + es.mu.vers.UnrefLocked() + } + return nil +} + +// Get implements the Reader interface. +func (es *EventuallyFileOnlySnapshot) Get(key []byte) (value []byte, closer io.Closer, err error) { + // TODO(jackson): Use getInternal. + iter, err := es.NewIter(nil) + if err != nil { + return nil, nil, err + } + var valid bool + if es.db.opts.Comparer.Split != nil { + valid = iter.SeekPrefixGE(key) + } else { + valid = iter.SeekGE(key) + } + if !valid { + if err = firstError(iter.Error(), iter.Close()); err != nil { + return nil, nil, err + } + return nil, nil, ErrNotFound + } + if !es.db.equal(iter.Key(), key) { + return nil, nil, firstError(iter.Close(), ErrNotFound) + } + return iter.Value(), iter, nil +} + +// NewIter returns an iterator that is unpositioned (Iterator.Valid() will +// return false). The iterator can be positioned via a call to SeekGE, +// SeekLT, First or Last. +func (es *EventuallyFileOnlySnapshot) NewIter(o *IterOptions) (*Iterator, error) { + return es.NewIterWithContext(context.Background(), o) +} + +// NewIterWithContext is like NewIter, and additionally accepts a context for +// tracing. +func (es *EventuallyFileOnlySnapshot) NewIterWithContext( + ctx context.Context, o *IterOptions, +) (*Iterator, error) { + select { + case <-es.closed: + panic(ErrClosed) + default: + } + + es.mu.Lock() + defer es.mu.Unlock() + if es.mu.vers != nil { + sOpts := snapshotIterOpts{seqNum: es.seqNum, vers: es.mu.vers} + return es.db.newIter(ctx, nil /* batch */, sOpts, o), nil + } + + if es.excised.Load() { + return nil, ErrSnapshotExcised + } + sOpts := snapshotIterOpts{seqNum: es.seqNum} + iter := es.db.newIter(ctx, nil /* batch */, sOpts, o) + + // If excised is true, then keys relevant to the snapshot might not be + // present in the readState being used by the iterator. Error out. + if es.excised.Load() { + iter.Close() + return nil, ErrSnapshotExcised + } + return iter, nil +} + +// ScanInternal scans all internal keys within the specified bounds, truncating +// any rangedels and rangekeys to those bounds. For use when an external user +// needs to be aware of all internal keys that make up a key range. +// +// See comment on db.ScanInternal for the behaviour that can be expected of +// point keys deleted by range dels and keys masked by range keys. +func (es *EventuallyFileOnlySnapshot) ScanInternal( + ctx context.Context, + lower, upper []byte, + visitPointKey func(key *InternalKey, value LazyValue, iterInfo IteratorLevel) error, + visitRangeDel func(start, end []byte, seqNum uint64) error, + visitRangeKey func(start, end []byte, keys []rangekey.Key) error, + visitSharedFile func(sst *SharedSSTMeta) error, +) error { + if es.db == nil { + panic(ErrClosed) + } + if es.excised.Load() { + return ErrSnapshotExcised + } + var sOpts snapshotIterOpts + es.mu.Lock() + if es.mu.vers != nil { + sOpts = snapshotIterOpts{ + seqNum: es.seqNum, + vers: es.mu.vers, + } + } else { + sOpts = snapshotIterOpts{ + seqNum: es.seqNum, + } + } + es.mu.Unlock() + opts := &scanInternalOptions{ + IterOptions: IterOptions{ + KeyTypes: IterKeyTypePointsAndRanges, + LowerBound: lower, + UpperBound: upper, + }, + visitPointKey: visitPointKey, + visitRangeDel: visitRangeDel, + visitRangeKey: visitRangeKey, + visitSharedFile: visitSharedFile, + skipSharedLevels: visitSharedFile != nil, + } + iter := es.db.newInternalIter(sOpts, opts) + defer iter.close() + + // If excised is true, then keys relevant to the snapshot might not be + // present in the readState being used by the iterator. Error out. + if es.excised.Load() { + return ErrSnapshotExcised + } + + return scanInternalImpl(ctx, lower, upper, iter, opts) +} diff --git a/vendor/github.com/cockroachdb/pebble/sstable/block.go b/vendor/github.com/cockroachdb/pebble/sstable/block.go index 864bb68..c6345ea 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/block.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/block.go @@ -10,7 +10,6 @@ import ( "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" - "github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/invariants" "github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/manual" @@ -47,7 +46,10 @@ type blockWriter struct { // ensuring that any of the keys with the same prefix can be used to // assemble the full key when the prefix does change. restarts []uint32 - curKey []byte + // Do not read curKey directly from outside blockWriter since it can have + // the InternalKeyKindSSTableInternalObsoleteBit set. Use getCurKey() or + // getCurUserKey() instead. + curKey []byte // curValue excludes the optional prefix provided to // storeWithOptionalValuePrefix. curValue []byte @@ -79,6 +81,20 @@ const setHasSameKeyPrefixRestartMask uint32 = 1 << 31 const restartMaskLittleEndianHighByteWithoutSetHasSamePrefix byte = 0b0111_1111 const restartMaskLittleEndianHighByteOnlySetHasSamePrefix byte = 0b1000_0000 +func (w *blockWriter) getCurKey() InternalKey { + k := base.DecodeInternalKey(w.curKey) + k.Trailer = k.Trailer & trailerObsoleteMask + return k +} + +func (w *blockWriter) getCurUserKey() []byte { + n := len(w.curKey) - base.InternalTrailerLen + if n < 0 { + panic(errors.AssertionFailedf("corrupt key in blockWriter buffer")) + } + return w.curKey[:n:n] +} + // If !addValuePrefix, the valuePrefix is ignored. func (w *blockWriter) storeWithOptionalValuePrefix( keySize int, @@ -191,12 +207,21 @@ func (w *blockWriter) storeWithOptionalValuePrefix( func (w *blockWriter) add(key InternalKey, value []byte) { w.addWithOptionalValuePrefix( - key, value, len(key.UserKey), false, 0, false) + key, false, value, len(key.UserKey), false, 0, false) } // Callers that always set addValuePrefix to false should use add() instead. +// +// isObsolete indicates whether this key-value pair is obsolete in this +// sstable (only applicable when writing data blocks) -- see the comment in +// table.go and the longer one in format.go. addValuePrefix adds a 1 byte +// prefix to the value, specified in valuePrefix -- this is used for data +// blocks in TableFormatPebblev3 onwards for SETs (see the comment in +// format.go, with more details in value_block.go). setHasSameKeyPrefix is +// also used in TableFormatPebblev3 onwards for SETs. func (w *blockWriter) addWithOptionalValuePrefix( key InternalKey, + isObsolete bool, value []byte, maxSharedKeyLen int, addValuePrefix bool, @@ -210,6 +235,9 @@ func (w *blockWriter) addWithOptionalValuePrefix( w.curKey = make([]byte, 0, size*2) } w.curKey = w.curKey[:size] + if isObsolete { + key.Trailer = key.Trailer | trailerObsoleteBit + } key.Encode(w.curKey) w.storeWithOptionalValuePrefix( @@ -371,16 +399,16 @@ type blockIter struct { // For a block encoded with a restart interval of 1, cached and cachedBuf // will not be used as there are no prefix compressed entries between the // restart points. - cached []blockEntry - cachedBuf []byte - cacheHandle cache.Handle - // The first key in the block. This is used by the caller to set bounds + cached []blockEntry + cachedBuf []byte + handle bufferHandle // for block iteration for already loaded blocks. - firstKey InternalKey + firstUserKey []byte lazyValueHandling struct { vbr *valueBlockReader hasValuePrefix bool } + hideObsoletePoints bool } // blockIter implements the base.InternalIterator interface. @@ -388,14 +416,16 @@ var _ base.InternalIterator = (*blockIter)(nil) func newBlockIter(cmp Compare, block block) (*blockIter, error) { i := &blockIter{} - return i, i.init(cmp, block, 0) + return i, i.init(cmp, block, 0, false) } func (i *blockIter) String() string { return "block" } -func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error { +func (i *blockIter) init( + cmp Compare, block block, globalSeqNum uint64, hideObsoletePoints bool, +) error { numRestarts := int32(binary.LittleEndian.Uint32(block[len(block)-4:])) if numRestarts == 0 { return base.CorruptionErrorf("pebble/table: invalid table (block has no restart points)") @@ -408,6 +438,7 @@ func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error { i.data = block i.fullKey = i.fullKey[:0] i.val = nil + i.hideObsoletePoints = hideObsoletePoints i.clearCache() if i.restarts > 0 { if err := i.readFirstKey(); err != nil { @@ -415,15 +446,21 @@ func (i *blockIter) init(cmp Compare, block block, globalSeqNum uint64) error { } } else { // Block is empty. - i.firstKey = InternalKey{} + i.firstUserKey = nil } return nil } -func (i *blockIter) initHandle(cmp Compare, block cache.Handle, globalSeqNum uint64) error { - i.cacheHandle.Release() - i.cacheHandle = block - return i.init(cmp, block.Get(), globalSeqNum) +// NB: two cases of hideObsoletePoints: +// - Local sstable iteration: globalSeqNum will be set iff the sstable was +// ingested. +// - Foreign sstable iteration: globalSeqNum is always set. +func (i *blockIter) initHandle( + cmp Compare, block bufferHandle, globalSeqNum uint64, hideObsoletePoints bool, +) error { + i.handle.Release() + i.handle = block + return i.init(cmp, block.Get(), globalSeqNum, hideObsoletePoints) } func (i *blockIter) invalidate() { @@ -590,24 +627,27 @@ func (i *blockIter) readFirstKey() error { // Manually inlining base.DecodeInternalKey provides a 5-10% speedup on // BlockIter benchmarks. if n := len(firstKey) - 8; n >= 0 { - i.firstKey.Trailer = binary.LittleEndian.Uint64(firstKey[n:]) - i.firstKey.UserKey = firstKey[:n:n] - if i.globalSeqNum != 0 { - i.firstKey.SetSeqNum(i.globalSeqNum) - } + i.firstUserKey = firstKey[:n:n] } else { - i.firstKey.Trailer = uint64(InternalKeyKindInvalid) - i.firstKey.UserKey = nil + i.firstUserKey = nil return base.CorruptionErrorf("pebble/table: invalid firstKey in block") } return nil } -func (i *blockIter) decodeInternalKey(key []byte) { +// The sstable internal obsolete bit is set when writing a block and unset by +// blockIter, so no code outside block writing/reading code ever sees it. +const trailerObsoleteBit = uint64(base.InternalKeyKindSSTableInternalObsoleteBit) +const trailerObsoleteMask = (InternalKeySeqNumMax << 8) | uint64(base.InternalKeyKindSSTableInternalObsoleteMask) + +func (i *blockIter) decodeInternalKey(key []byte) (hiddenPoint bool) { // Manually inlining base.DecodeInternalKey provides a 5-10% speedup on // BlockIter benchmarks. if n := len(key) - 8; n >= 0 { - i.ikey.Trailer = binary.LittleEndian.Uint64(key[n:]) + trailer := binary.LittleEndian.Uint64(key[n:]) + hiddenPoint = i.hideObsoletePoints && + (trailer&trailerObsoleteBit != 0) + i.ikey.Trailer = trailer & trailerObsoleteMask i.ikey.UserKey = key[:n:n] if i.globalSeqNum != 0 { i.ikey.SetSeqNum(i.globalSeqNum) @@ -616,6 +656,7 @@ func (i *blockIter) decodeInternalKey(key []byte) { i.ikey.Trailer = uint64(InternalKeyKindInvalid) i.ikey.UserKey = nil } + return hiddenPoint } func (i *blockIter) clearCache() { @@ -640,6 +681,10 @@ func (i *blockIter) cacheEntry() { i.cachedBuf = append(i.cachedBuf, i.key...) } +func (i *blockIter) getFirstUserKey() []byte { + return i.firstUserKey +} + // SeekGE implements internalIterator.SeekGE, as documented in the pebble // package. func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, base.LazyValue) { @@ -647,7 +692,6 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba panic(errors.AssertionFailedf("invalidated blockIter used")) } - ikey := base.MakeSearchKey(key) i.clearCache() // Find the index of the smallest restart point whose key is > the key // sought; index will be numRestarts if there is no such restart point. @@ -702,23 +746,23 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba ptr = unsafe.Pointer(uintptr(ptr) + 5) } - // Manually inlining base.DecodeInternalKey provides a 5-10% speedup on - // BlockIter benchmarks. + // Manually inlining part of base.DecodeInternalKey provides a 5-10% + // speedup on BlockIter benchmarks. s := getBytes(ptr, int(v1)) - var k InternalKey + var k []byte if n := len(s) - 8; n >= 0 { - k.Trailer = binary.LittleEndian.Uint64(s[n:]) - k.UserKey = s[:n:n] - // NB: We can't have duplicate keys if the globalSeqNum != 0, so we - // leave the seqnum on this key as 0 as it won't affect our search - // since ikey has the maximum seqnum. - } else { - k.Trailer = uint64(InternalKeyKindInvalid) + k = s[:n:n] } + // Else k is invalid, and left as nil - if base.InternalCompare(i.cmp, ikey, k) >= 0 { + if i.cmp(key, k) > 0 { + // The search key is greater than the user key at this restart point. + // Search beyond this restart point, since we are trying to find the + // first restart point with a user key >= the search key. index = h + 1 // preserves f(i-1) == false } else { + // k >= search key, so prune everything after index (since index + // satisfies the property we are looking for). upper = h // preserves f(j) == true } } @@ -726,21 +770,25 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba // => answer is index. } - // Since keys are strictly increasing, if index > 0 then the restart point at - // index-1 will be the largest whose key is <= the key sought. If index == - // 0, then all keys in this block are larger than the key sought, and offset - // remains at zero. + // index is the first restart point with key >= search key. Define the keys + // between a restart point and the next restart point as belonging to that + // restart point. + // + // Since keys are strictly increasing, if index > 0 then the restart point + // at index-1 will be the first one that has some keys belonging to it that + // could be equal to the search key. If index == 0, then all keys in this + // block are larger than the key sought, and offset remains at zero. if index > 0 { i.offset = decodeRestart(i.data[i.restarts+4*(index-1):]) } i.readEntry() - i.decodeInternalKey(i.key) + hiddenPoint := i.decodeInternalKey(i.key) // Iterate from that restart point to somewhere >= the key sought. if !i.valid() { return nil, base.LazyValue{} } - if base.InternalCompare(i.cmp, i.ikey, ikey) >= 0 { + if !hiddenPoint && i.cmp(i.ikey.UserKey, key) >= 0 { // Initialize i.lazyValue if !i.lazyValueHandling.hasValuePrefix || base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { @@ -753,7 +801,7 @@ func (i *blockIter) SeekGE(key []byte, flags base.SeekGEFlags) (*InternalKey, ba return &i.ikey, i.lazyValue } for i.Next(); i.valid(); i.Next() { - if base.InternalCompare(i.cmp, i.ikey, ikey) >= 0 { + if i.cmp(i.ikey.UserKey, key) >= 0 { // i.Next() has already initialized i.lazyValue. return &i.ikey, i.lazyValue } @@ -777,8 +825,6 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba panic(errors.AssertionFailedf("invalidated blockIter used")) } - ikey := base.MakeSearchKey(key) - i.clearCache() // Find the index of the smallest restart point whose key is >= the key // sought; index will be numRestarts if there is no such restart point. @@ -833,23 +879,23 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba ptr = unsafe.Pointer(uintptr(ptr) + 5) } - // Manually inlining base.DecodeInternalKey provides a 5-10% speedup on - // BlockIter benchmarks. + // Manually inlining part of base.DecodeInternalKey provides a 5-10% + // speedup on BlockIter benchmarks. s := getBytes(ptr, int(v1)) - var k InternalKey + var k []byte if n := len(s) - 8; n >= 0 { - k.Trailer = binary.LittleEndian.Uint64(s[n:]) - k.UserKey = s[:n:n] - // NB: We can't have duplicate keys if the globalSeqNum != 0, so we - // leave the seqnum on this key as 0 as it won't affect our search - // since ikey has the maximum seqnum. - } else { - k.Trailer = uint64(InternalKeyKindInvalid) + k = s[:n:n] } + // Else k is invalid, and left as nil - if base.InternalCompare(i.cmp, ikey, k) > 0 { + if i.cmp(key, k) > 0 { + // The search key is greater than the user key at this restart point. + // Search beyond this restart point, since we are trying to find the + // first restart point with a user key >= the search key. index = h + 1 // preserves f(i-1) == false } else { + // k >= search key, so prune everything after index (since index + // satisfies the property we are looking for). upper = h // preserves f(j) == true } } @@ -857,8 +903,15 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba // => answer is index. } - // Since keys are strictly increasing, if index > 0 then the restart point at - // index-1 will be the largest whose key is < the key sought. + // index is the first restart point with key >= search key. Define the keys + // between a restart point and the next restart point as belonging to that + // restart point. Note that index could be equal to i.numRestarts, i.e., we + // are past the last restart. + // + // Since keys are strictly increasing, if index > 0 then the restart point + // at index-1 will be the first one that has some keys belonging to it that + // are less than the search key. If index == 0, then all keys in this block + // are larger than the search key, so there is no match. targetOffset := i.restarts if index > 0 { i.offset = decodeRestart(i.data[i.restarts+4*(index-1):]) @@ -881,24 +934,41 @@ func (i *blockIter) SeekLT(key []byte, flags base.SeekLTFlags) (*InternalKey, ba for { i.offset = i.nextOffset i.readEntry() - i.decodeInternalKey(i.key) + // When hidden keys are common, there is additional optimization possible + // by not caching entries that are hidden (note that some calls to + // cacheEntry don't decode the internal key before caching, but checking + // whether a key is hidden does not require full decoding). However, we do + // need to use the blockEntry.offset in the cache for the first entry at + // the reset point to do the binary search when the cache is empty -- so + // we would need to cache that first entry (though not the key) even if + // was hidden. Our current assumption is that if there are large numbers + // of hidden keys we will be able to skip whole blocks (using block + // property filters) so we don't bother optimizing. + hiddenPoint := i.decodeInternalKey(i.key) - if i.cmp(i.ikey.UserKey, ikey.UserKey) >= 0 { + // NB: we don't use the hiddenPoint return value of decodeInternalKey + // since we want to stop as soon as we reach a key >= ikey.UserKey, so + // that we can reverse. + if i.cmp(i.ikey.UserKey, key) >= 0 { // The current key is greater than or equal to our search key. Back up to // the previous key which was less than our search key. Note that this for // loop will execute at least once with this if-block not being true, so // the key we are backing up to is the last one this loop cached. - i.Prev() - // i.Prev() has already initialized i.lazyValue. - return &i.ikey, i.lazyValue + return i.Prev() } if i.nextOffset >= targetOffset { - // We've reached the end of the current restart block. Return the current - // key. When the restart interval is 1, the first iteration of the for - // loop will bring us here. In that case ikey is backed by the block so - // we get the desired key stability guarantee for the lifetime of the - // blockIter. + // We've reached the end of the current restart block. Return the + // current key if not hidden, else call Prev(). + // + // When the restart interval is 1, the first iteration of the for loop + // will bring us here. In that case ikey is backed by the block so we + // get the desired key stability guarantee for the lifetime of the + // blockIter. That is, we never cache anything and therefore never + // return a key backed by cachedBuf. + if hiddenPoint { + return i.Prev() + } break } @@ -932,7 +1002,10 @@ func (i *blockIter) First() (*InternalKey, base.LazyValue) { } i.clearCache() i.readEntry() - i.decodeInternalKey(i.key) + hiddenPoint := i.decodeInternalKey(i.key) + if hiddenPoint { + return i.Next() + } if !i.lazyValueHandling.hasValuePrefix || base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { i.lazyValue = base.MakeInPlaceValue(i.val) @@ -971,7 +1044,10 @@ func (i *blockIter) Last() (*InternalKey, base.LazyValue) { i.readEntry() } - i.decodeInternalKey(i.key) + hiddenPoint := i.decodeInternalKey(i.key) + if hiddenPoint { + return i.Prev() + } if !i.lazyValueHandling.hasValuePrefix || base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { i.lazyValue = base.MakeInPlaceValue(i.val) @@ -1001,6 +1077,7 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) { i.clearCache() } +start: i.offset = i.nextOffset if !i.valid() { return nil, base.LazyValue{} @@ -1008,11 +1085,17 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) { i.readEntry() // Manually inlined version of i.decodeInternalKey(i.key). if n := len(i.key) - 8; n >= 0 { - i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:]) + trailer := binary.LittleEndian.Uint64(i.key[n:]) + hiddenPoint := i.hideObsoletePoints && + (trailer&trailerObsoleteBit != 0) + i.ikey.Trailer = trailer & trailerObsoleteMask i.ikey.UserKey = i.key[:n:n] if i.globalSeqNum != 0 { i.ikey.SetSeqNum(i.globalSeqNum) } + if hiddenPoint { + goto start + } } else { i.ikey.Trailer = uint64(InternalKeyKindInvalid) i.ikey.UserKey = nil @@ -1028,8 +1111,8 @@ func (i *blockIter) Next() (*InternalKey, base.LazyValue) { return &i.ikey, i.lazyValue } -// nextPrefix is used for implementing NPrefix. -func (i *blockIter) nextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { +// NextPrefix implements (base.InternalIterator).NextPrefix. +func (i *blockIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { if i.lazyValueHandling.hasValuePrefix { return i.nextPrefixV3(succKey) } @@ -1154,6 +1237,7 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue) // The trailer is written in little endian, so the key kind is the first // byte in the trailer that is encoded in the slice [unshared-8:unshared]. keyKind := InternalKeyKind((*[manual.MaxArrayLen]byte)(ptr)[unshared-8]) + keyKind = keyKind & base.InternalKeyKindSSTableInternalObsoleteMask prefixChanged := false if keyKind == InternalKeyKindSet { if invariants.Enabled && value == 0 { @@ -1269,8 +1353,12 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue) i.key = i.fullKey } // Manually inlined version of i.decodeInternalKey(i.key). + hiddenPoint := false if n := len(i.key) - 8; n >= 0 { - i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:]) + trailer := binary.LittleEndian.Uint64(i.key[n:]) + hiddenPoint = i.hideObsoletePoints && + (trailer&trailerObsoleteBit != 0) + i.ikey.Trailer = trailer & trailerObsoleteMask i.ikey.UserKey = i.key[:n:n] if i.globalSeqNum != 0 { i.ikey.SetSeqNum(i.globalSeqNum) @@ -1286,6 +1374,9 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue) } if prefixChanged || i.cmp(i.ikey.UserKey, succKey) >= 0 { // Prefix has changed. + if hiddenPoint { + return i.Next() + } if invariants.Enabled && !i.lazyValueHandling.hasValuePrefix { panic(errors.AssertionFailedf("nextPrefixV3 being run for non-v3 sstable")) } @@ -1307,23 +1398,11 @@ func (i *blockIter) nextPrefixV3(succKey []byte) (*InternalKey, base.LazyValue) return i.SeekGE(succKey, base.SeekGEFlagsNone) } -// NextPrefix implements (base.InternalIterator).NextPrefix -func (i *blockIter) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { - const nextsBeforeSeek = 3 - k, v := i.Next() - for j := 1; k != nil && i.cmp(k.UserKey, succKey) < 0; j++ { - if j >= nextsBeforeSeek { - return i.SeekGE(succKey, base.SeekGEFlagsNone) - } - k, v = i.Next() - } - return k, v -} - // Prev implements internalIterator.Prev, as documented in the pebble // package. func (i *blockIter) Prev() (*InternalKey, base.LazyValue) { - if n := len(i.cached) - 1; n >= 0 { +start: + for n := len(i.cached) - 1; n >= 0; n-- { i.nextOffset = i.offset e := &i.cached[n] i.offset = e.offset @@ -1331,7 +1410,13 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) { // Manually inlined version of i.decodeInternalKey(i.key). i.key = i.cachedBuf[e.keyStart:e.keyEnd] if n := len(i.key) - 8; n >= 0 { - i.ikey.Trailer = binary.LittleEndian.Uint64(i.key[n:]) + trailer := binary.LittleEndian.Uint64(i.key[n:]) + hiddenPoint := i.hideObsoletePoints && + (trailer&trailerObsoleteBit != 0) + if hiddenPoint { + continue + } + i.ikey.Trailer = trailer & trailerObsoleteMask i.ikey.UserKey = i.key[:n:n] if i.globalSeqNum != 0 { i.ikey.SetSeqNum(i.globalSeqNum) @@ -1373,6 +1458,8 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) { // index ≤ h < upper offset := decodeRestart(i.data[i.restarts+4*h:]) if offset < targetOffset { + // Looking for the first restart that has offset >= targetOffset, so + // ignore h and earlier. index = h + 1 // preserves f(i-1) == false } else { upper = h // preserves f(j) == true @@ -1382,20 +1469,35 @@ func (i *blockIter) Prev() (*InternalKey, base.LazyValue) { // => answer is index. } + // index is first restart with offset >= targetOffset. Note that + // targetOffset may not be at a restart point since one can call Prev() + // after Next() (so the cache was not populated) and targetOffset refers to + // the current entry. index-1 must have an offset < targetOffset (it can't + // be equal to targetOffset since the binary search would have selected that + // as the index). i.offset = 0 if index > 0 { i.offset = decodeRestart(i.data[i.restarts+4*(index-1):]) } + // TODO(sumeer): why is the else case not an error given targetOffset is a + // valid offset. i.readEntry() + // We stop when i.nextOffset == targetOffset since the targetOffset is the + // entry we are stepping back from, and we don't need to cache the entry + // before it, since it is the candidate to return. for i.nextOffset < targetOffset { i.cacheEntry() i.offset = i.nextOffset i.readEntry() } - i.decodeInternalKey(i.key) + hiddenPoint := i.decodeInternalKey(i.key) + if hiddenPoint { + // Use the cache. + goto start + } if !i.lazyValueHandling.hasValuePrefix || base.TrailerKind(i.ikey.Trailer) != InternalKeyKindSet { i.lazyValue = base.MakeInPlaceValue(i.val) @@ -1425,8 +1527,8 @@ func (i *blockIter) Error() error { // Close implements internalIterator.Close, as documented in the pebble // package. func (i *blockIter) Close() error { - i.cacheHandle.Release() - i.cacheHandle = cache.Handle{} + i.handle.Release() + i.handle = bufferHandle{} i.val = nil i.lazyValue = base.LazyValue{} i.lazyValueHandling.vbr = nil @@ -1468,6 +1570,10 @@ type fragmentBlockIter struct { err error dir int8 closeHook func(i keyspan.FragmentIterator) error + + // elideSameSeqnum, if true, returns only the first-occurring (in forward + // order) Key for each sequence number. + elideSameSeqnum bool } func (i *fragmentBlockIter) resetForReuse() fragmentBlockIter { @@ -1496,6 +1602,24 @@ func (i *fragmentBlockIter) decodeSpanKeys(k *InternalKey, internalValue []byte) } } +func (i *fragmentBlockIter) elideKeysOfSameSeqNum() { + if invariants.Enabled { + if !i.elideSameSeqnum || len(i.span.Keys) == 0 { + panic("elideKeysOfSameSeqNum called when it should not be") + } + } + lastSeqNum := i.span.Keys[0].SeqNum() + k := 1 + for j := 1; j < len(i.span.Keys); j++ { + if lastSeqNum != i.span.Keys[j].SeqNum() { + lastSeqNum = i.span.Keys[j].SeqNum() + i.span.Keys[k] = i.span.Keys[j] + k++ + } + } + i.span.Keys = i.span.Keys[:k] +} + // gatherForward gathers internal keys with identical bounds. Keys defined over // spans of the keyspace are fragmented such that any overlapping key spans have // identical bounds. When these spans are persisted to a range deletion or range @@ -1545,6 +1669,9 @@ func (i *fragmentBlockIter) gatherForward(k *InternalKey, lazyValue base.LazyVal k, lazyValue = i.blockIter.Next() internalValue = lazyValue.InPlaceValue() } + if i.elideSameSeqnum && len(i.span.Keys) > 0 { + i.elideKeysOfSameSeqNum() + } // i.blockIter is positioned over the first internal key for the next span. return &i.span } @@ -1604,6 +1731,9 @@ func (i *fragmentBlockIter) gatherBackward(k *InternalKey, lazyValue base.LazyVa // Backwards iteration encounters internal keys in the wrong order. keyspan.SortKeysByTrailer(&i.span.Keys) + if i.elideSameSeqnum && len(i.span.Keys) > 0 { + i.elideKeysOfSameSeqNum() + } return &i.span } diff --git a/vendor/github.com/cockroachdb/pebble/sstable/block_property.go b/vendor/github.com/cockroachdb/pebble/sstable/block_property.go index 1e986fb..d85a2b2 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/block_property.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/block_property.go @@ -9,6 +9,7 @@ import ( "fmt" "math" "sync" + "unsafe" "github.com/cockroachdb/pebble/internal/base" "github.com/cockroachdb/pebble/internal/rangekey" @@ -117,7 +118,7 @@ type BlockPropertyCollector interface { } // SuffixReplaceableBlockCollector is an extension to the BlockPropertyCollector -// interface that allows a block property collector to indicate the it supports +// interface that allows a block property collector to indicate that it supports // being *updated* during suffix replacement, i.e. when an existing SST in which // all keys have the same key suffix is updated to have a new suffix. // @@ -189,13 +190,13 @@ type BoundLimitedBlockPropertyFilter interface { // indicates that the filter may be used to filter blocks that exclusively // contain keys ≥ `key`, so long as the blocks' keys also satisfy the upper // bound. - KeyIsWithinLowerBound(key *InternalKey) bool + KeyIsWithinLowerBound(key []byte) bool // KeyIsWithinUpperBound tests whether the provided internal key falls // within the current upper bound of the filter. A true return value // indicates that the filter may be used to filter blocks that exclusively // contain keys ≤ `key`, so long as the blocks' keys also satisfy the lower // bound. - KeyIsWithinUpperBound(key *InternalKey) bool + KeyIsWithinUpperBound(key []byte) bool } // BlockIntervalCollector is a helper implementation of BlockPropertyCollector @@ -643,15 +644,21 @@ func (f *BlockPropertiesFilterer) intersectsUserPropsAndFinishInit( // considered intersecting. continue } - byteProps := []byte(props) - if len(byteProps) < 1 { + if len(props) < 1 { return false, base.CorruptionErrorf( "block properties for %s is corrupted", f.filters[i].Name()) } - shortID := shortID(byteProps[0]) - intersects, err := f.filters[i].Intersects(byteProps[1:]) - if err != nil || !intersects { - return false, err + shortID := shortID(props[0]) + { + // Use an unsafe conversion to avoid allocating. Intersects() is not + // supposed to modify the given slice. + // Note that unsafe.StringData only works if the string is not empty + // (which we already checked). + byteProps := unsafe.Slice(unsafe.StringData(props), len(props)) + intersects, err := f.filters[i].Intersects(byteProps[1:]) + if err != nil || !intersects { + return false, err + } } // Intersects the sstable, so need to use this filter when // deciding whether to read blocks. @@ -684,12 +691,11 @@ func (f *BlockPropertiesFilterer) intersectsUserPropsAndFinishInit( // be unused within this file. return true, nil } - byteProps := []byte(props) - if len(byteProps) < 1 { + if len(props) < 1 { return false, base.CorruptionErrorf( "block properties for %s is corrupted", f.boundLimitedFilter.Name()) } - f.boundLimitedShortID = int(byteProps[0]) + f.boundLimitedShortID = int(props[0]) // We don't check for table-level intersection for the bound-limited filter. // The bound-limited filter is treated as vacuously intersecting. diff --git a/vendor/github.com/cockroachdb/pebble/sstable/buffer_pool.go b/vendor/github.com/cockroachdb/pebble/sstable/buffer_pool.go new file mode 100644 index 0000000..2e98d44 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/sstable/buffer_pool.go @@ -0,0 +1,148 @@ +// Copyright 2023 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 sstable + +import ( + "github.com/cockroachdb/errors" + "github.com/cockroachdb/pebble/internal/cache" +) + +// A bufferHandle is a handle to manually-managed memory. The handle may point +// to a block in the block cache (h.Get() != nil), or a buffer that exists +// outside the block cache allocated from a BufferPool (b.Valid()). +type bufferHandle struct { + h cache.Handle + b Buf +} + +// Get retrieves the underlying buffer referenced by the handle. +func (bh bufferHandle) Get() []byte { + if v := bh.h.Get(); v != nil { + return v + } else if bh.b.p != nil { + return bh.b.p.pool[bh.b.i].b + } + return nil +} + +// Release releases the buffer, either back to the block cache or BufferPool. +func (bh bufferHandle) Release() { + bh.h.Release() + bh.b.Release() +} + +// A BufferPool holds a pool of buffers for holding sstable blocks. An initial +// size of the pool is provided on Init, but a BufferPool will grow to meet the +// largest working set size. It'll never shrink. When a buffer is released, the +// BufferPool recycles the buffer for future allocations. +// +// A BufferPool should only be used for short-lived allocations with +// well-understood working set sizes to avoid excessive memory consumption. +// +// BufferPool is not thread-safe. +type BufferPool struct { + // pool contains all the buffers held by the pool, including buffers that + // are in-use. For every i < len(pool): pool[i].v is non-nil. + pool []allocedBuffer +} + +type allocedBuffer struct { + v *cache.Value + // b holds the current byte slice. It's backed by v, but may be a subslice + // of v's memory while the buffer is in-use [ len(b) ≤ len(v.Buf()) ]. + // + // If the buffer is not currently in-use, b is nil. When being recycled, the + // BufferPool.Alloc will reset b to be a subslice of v.Buf(). + b []byte +} + +// Init initializes the pool with an initial working set buffer size of +// `initialSize`. +func (p *BufferPool) Init(initialSize int) { + *p = BufferPool{ + pool: make([]allocedBuffer, 0, initialSize), + } +} + +// initPreallocated is like Init but for internal sstable package use in +// instances where a pre-allocated slice of []allocedBuffer already exists. It's +// used to avoid an extra allocation initializing BufferPool.pool. +func (p *BufferPool) initPreallocated(pool []allocedBuffer) { + *p = BufferPool{ + pool: pool[:0], + } +} + +// Release releases all buffers held by the pool and resets the pool to an +// uninitialized state. +func (p *BufferPool) Release() { + for i := range p.pool { + if p.pool[i].b != nil { + panic(errors.AssertionFailedf("Release called on a BufferPool with in-use buffers")) + } + cache.Free(p.pool[i].v) + } + *p = BufferPool{} +} + +// Alloc allocates a new buffer of size n. If the pool already holds a buffer at +// least as large as n, the pooled buffer is used instead. +// +// Alloc is O(MAX(N,M)) where N is the largest number of concurrently in-use +// buffers allocated and M is the initialSize passed to Init. +func (p *BufferPool) Alloc(n int) Buf { + unusableBufferIdx := -1 + for i := 0; i < len(p.pool); i++ { + if p.pool[i].b == nil { + if len(p.pool[i].v.Buf()) >= n { + p.pool[i].b = p.pool[i].v.Buf()[:n] + return Buf{p: p, i: i} + } + unusableBufferIdx = i + } + } + + // If we would need to grow the size of the pool to allocate another buffer, + // but there was a slot available occupied by a buffer that's just too + // small, replace the too-small buffer. + if len(p.pool) == cap(p.pool) && unusableBufferIdx >= 0 { + i := unusableBufferIdx + cache.Free(p.pool[i].v) + p.pool[i].v = cache.Alloc(n) + p.pool[i].b = p.pool[i].v.Buf() + return Buf{p: p, i: i} + } + + // Allocate a new buffer. + v := cache.Alloc(n) + p.pool = append(p.pool, allocedBuffer{v: v, b: v.Buf()[:n]}) + return Buf{p: p, i: len(p.pool) - 1} +} + +// A Buf holds a reference to a manually-managed, pooled byte buffer. +type Buf struct { + p *BufferPool + // i holds the index into p.pool where the buffer may be found. This scheme + // avoids needing to allocate the handle to the buffer on the heap at the + // cost of copying two words instead of one. + i int +} + +// Valid returns true if the buf holds a valid buffer. +func (b Buf) Valid() bool { + return b.p != nil +} + +// Release releases the buffer back to the pool. +func (b *Buf) Release() { + if b.p == nil { + return + } + // Clear the allocedBuffer's byte slice. This signals the allocated buffer + // is no longer in use and a future call to BufferPool.Alloc may reuse this + // buffer. + b.p.pool[b.i].b = nil + b.p = nil +} diff --git a/vendor/github.com/cockroachdb/pebble/sstable/compression.go b/vendor/github.com/cockroachdb/pebble/sstable/compression.go index 4dd5bea..0db70c8 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/compression.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/compression.go @@ -52,10 +52,10 @@ func decompressInto(blockType blockType, compressed []byte, buf []byte) ([]byte, return result, nil } -// decompressBlock decompresses an SST block, with space allocated from a cache. +// decompressBlock decompresses an SST block, with manually-allocated space. // NB: If decompressBlock returns (nil, nil), no decompression was necessary and // the caller may use `b` directly. -func decompressBlock(cache *cache.Cache, blockType blockType, b []byte) (*cache.Value, error) { +func decompressBlock(blockType blockType, b []byte) (*cache.Value, error) { if blockType == noCompressionBlockType { return nil, nil } diff --git a/vendor/github.com/cockroachdb/pebble/sstable/filter.go b/vendor/github.com/cockroachdb/pebble/sstable/filter.go index 2d73e12..7b2e1ab 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/filter.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/filter.go @@ -18,14 +18,32 @@ type FilterMetrics struct { Misses int64 } -var dummyFilterMetrics FilterMetrics +// FilterMetricsTracker is used to keep track of filter metrics. It contains the +// same metrics as FilterMetrics, but they can be updated atomically. An +// instance of FilterMetricsTracker can be passed to a Reader as a ReaderOption. +type FilterMetricsTracker struct { + // See FilterMetrics.Hits. + hits atomic.Int64 + // See FilterMetrics.Misses. + misses atomic.Int64 +} -func (m *FilterMetrics) readerApply(r *Reader) { +var _ ReaderOption = (*FilterMetricsTracker)(nil) + +func (m *FilterMetricsTracker) readerApply(r *Reader) { if r.tableFilter != nil { r.tableFilter.metrics = m } } +// Load returns the current values as FilterMetrics. +func (m *FilterMetricsTracker) Load() FilterMetrics { + return FilterMetrics{ + Hits: m.hits.Load(), + Misses: m.misses.Load(), + } +} + // BlockHandle is the file offset and length of a block. type BlockHandle struct { Offset, Length uint64 @@ -47,22 +65,24 @@ type filterWriter interface { type tableFilterReader struct { policy FilterPolicy - metrics *FilterMetrics + metrics *FilterMetricsTracker } func newTableFilterReader(policy FilterPolicy) *tableFilterReader { return &tableFilterReader{ policy: policy, - metrics: &dummyFilterMetrics, + metrics: nil, } } func (f *tableFilterReader) mayContain(data, key []byte) bool { mayContain := f.policy.MayContain(TableFilter, data, key) - if mayContain { - atomic.AddInt64(&f.metrics.Misses, 1) - } else { - atomic.AddInt64(&f.metrics.Hits, 1) + if f.metrics != nil { + if mayContain { + f.metrics.misses.Add(1) + } else { + f.metrics.hits.Add(1) + } } return mayContain } diff --git a/vendor/github.com/cockroachdb/pebble/sstable/format.go b/vendor/github.com/cockroachdb/pebble/sstable/format.go index a3c7300..82310a5 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/format.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/format.go @@ -23,15 +23,164 @@ const ( TableFormatRocksDBv2 TableFormatPebblev1 // Block properties. TableFormatPebblev2 // Range keys. - // TableFormatPebblev3 is not currently intended to subsume v2, as - // supporting value blocks adds a 1 byte prefix to each value. After - // thorough experimentation and some production experience, this may change. TableFormatPebblev3 // Value blocks. + TableFormatPebblev4 // DELSIZED tombstones. NumTableFormats - TableFormatMax = TableFormatPebblev3 + TableFormatMax = NumTableFormats - 1 ) +// TableFormatPebblev4, in addition to DELSIZED, introduces the use of +// InternalKeyKindSSTableInternalObsoleteBit. +// +// 1. Motivation +// +// We have various related problems caused by Pebble snapshots: +// +// - P1: RANGEDELs that delete points in the same sstable, but the points +// happen to not get deleted during compactions because of an open snapshot. +// This causes very expensive iteration, that has been observed in +// production deployments +// +// - P2: When iterating over a foreign sstable (in disaggregated storage), we +// need to do (a) point collapsing to expose at most one point per user key, +// (b) apply RANGEDELs in the sstable to hide deleted points in the same +// sstable. This per-sstable point collapsing iteration needs to be very +// efficient (ideally as efficient from a CPU perspective as iteration over +// regular sstables) since foreign sstables can be very long-lived -- one of +// the goals of disaggregated storage is to scale compute and disk bandwidth +// resources as a function of the hot (from a write perspective) data and +// not the whole data, so we don't want to have to rewrite foreign sstables +// solely to improve read performance. +// +// The ideal solution for P2 would allow user-facing reads to utilize the +// existing SST iterators (with slight modifications) and with no loss of +// efficiency. And for P1 and P2 we would like to skip whole blocks of +// overwritten/deleted points. Even when we can't skip whole blocks, avoiding +// key comparisons at iteration time to discover what points are deleted is +// very desirable, since keys can be long. +// +// We observe that: +// +// - Reads: +// - All user-facing reads in CockroachDB use iterators over the DB, hence +// have a higher read seqnum than all sstables (there are some rare cases +// that can violate this, but those are not important from a performance +// optimization perspective). +// +// - Certain internal-facing reads in CockroachDB use snapshots, but the +// snapshots are shortlived enough that most L5 and L6 sstables will have +// all seqnums lower than the snapshot seqnum. +// +// - Writes: +// - We already do key comparisons between points when writing the sstable +// to ensure that the sstable invariant (monotonically increasing internal +// keys) is not violated. So we know which points share the same userkey, +// and thereby which points are obsolete because there is a more recent +// point in the same sstable. +// +// - The compactionIter knows which point id deleted by a RANGEDEL even if +// the point does need to be written because of a snapshot. +// +// So this known information can be encoded in the sstable at write time and +// utilized for optimized reading. +// +// 2. Solution +// +// We primarily scope the solution to the following point kinds: SET, +// SETWITHDEL, DEL, DELSIZED, SINGLEDEL. These are the ones marked locally +// obsolete, i.e., obsolete within the sstable, and we can guarantee that at +// most one point will be exposed per user key. MERGE keys create more +// complexity: MERGE followed by MERGE causes multiple keys to not be +// obsolete. Same applies for MERGE followed by SET/SETWITHDEL/DEL*. Note +// that: +// +// - For regular sst iteration, the obsolete marking is a performance +// optimization, and multiple keys for the same userkey can be handled by +// higher layers in the iterator tree (specifically pebble.Iterator). +// +// - For foreign sst iteration, we disallow MERGEs to be written to such +// shared ssts (details below). +// +// The key kinds are marked with an obsolete bit +// (InternalKeyKindSSTableInternalObsoleteBit) when the key-value pair is +// obsolete. This marking is done within blockWriter, based on information +// passed to it by Writer. In turn, Writer uses a combination of key +// comparisons, and information provided by compactionIter to decide whether a +// key-value pair is obsolete. Additionally, a Pebble-internal +// BlockPropertyCollector (obsoleteKeyBlockPropertyCollector) is used to mark +// blocks where all key-value pairs are obsolete. Since the common case is +// non-obsolete blocks, this block property collector uses the empty byte +// slice to represent a non-obsolete block, which consumes no space in +// BlockHandleWithProperties.Props. +// +// At read time, the obsolete bit is only visible to the blockIter, which can +// be optionally configured to hide obsolete points. This hiding is only +// configured for data block iterators for sstables being read by user-facing +// iterators at a seqnum greater than the max seqnum in the sstable. +// Additionally, when this hiding is configured, a Pebble-internal block +// property filter (obsoleteKeyBlockPropertyFilter), is used to skip whole +// blocks that are obsolete. +// +// 2.1 Correctness +// +// Due to the level invariant, the sequence of seqnums for a user key in a +// sstable represents a contiguous subsequence of the seqnums for the userkey +// across the whole LSM, and is more recent than the seqnums in a sstable in a +// lower level. So exposing exactly one point from a sstable for a userkey +// will also mask the points for the userkey in lower levels. If we expose no +// point, because of RANGEDELs, that RANGEDEL will also mask the points in +// lower levels. +// +// Note that we do not need to do anything special at write time for +// SETWITHDEL and SINGLEDEL. This is because these key kinds are treated +// specially only by compactions, which do not hide obsolete points. For +// regular reads, SETWITHDEL behaves the same as SET and SINGLEDEL behaves the +// same as DEL. +// +// 2.2 Strictness and MERGE +// +// Setting the obsolete bit on point keys is advanced usage, so we support two +// modes, both of which must be truthful when setting the obsolete bit, but +// vary in when they don't set the obsolete bit. +// +// - Non-strict: In this mode, the bit does not need to be set for keys that +// are obsolete. Additionally, any sstable containing MERGE keys can only +// use this mode. An iterator over such an sstable, when configured to +// hideObsoletePoints, can expose multiple internal keys per user key, and +// can expose keys that are deleted by rangedels in the same sstable. This +// is the mode that non-advanced users should use. Pebble without +// disaggregated storage will also use this mode and will best-effort set +// the obsolete bit, to optimize iteration when snapshots have retained many +// obsolete keys. +// +// - Strict: In this mode, every obsolete key must have the obsolete bit set, +// and no MERGE keys are permitted. An iterator over such an sstable, when +// configured to hideObsoletePoints satisfies two properties: +// - S1: will expose at most one internal key per user key, which is the +// most recent one. +// - S2: will never expose keys that are deleted by rangedels in the same +// sstable. +// +// This is the mode for two use cases in disaggregated storage (which will +// exclude parts of the key space that has MERGEs), for levels that contain +// sstables that can become foreign sstables: +// - Pebble compaction output to these levels that can become foreign +// sstables. +// +// - CockroachDB ingest operations that can ingest into the levels that can +// become foreign sstables. Note, these are not sstables corresponding to +// copied data for CockroachDB range snapshots. This case occurs for +// operations like index backfills: these trivially satisfy the strictness +// criteria since they only write one key per userkey. +// +// TODO(sumeer): this latter case is not currently supported, since only +// Writer.AddWithForceObsolete calls are permitted for writing strict +// obsolete sstables. This is done to reduce the likelihood of bugs. One +// simple way to lift this limitation would be to disallow adding any +// RANGEDELs when a Pebble-external writer is trying to construct a strict +// obsolete sstable. + // ParseTableFormat parses the given magic bytes and version into its // corresponding internal TableFormat. func ParseTableFormat(magic []byte, version uint32) (TableFormat, error) { @@ -53,6 +202,8 @@ func ParseTableFormat(magic []byte, version uint32) (TableFormat, error) { return TableFormatPebblev2, nil case 3: return TableFormatPebblev3, nil + case 4: + return TableFormatPebblev4, nil default: return TableFormatUnspecified, base.CorruptionErrorf( "pebble/table: unsupported pebble format version %d", errors.Safe(version), @@ -78,6 +229,8 @@ func (f TableFormat) AsTuple() (string, uint32) { return pebbleDBMagic, 2 case TableFormatPebblev3: return pebbleDBMagic, 3 + case TableFormatPebblev4: + return pebbleDBMagic, 4 default: panic("sstable: unknown table format version tuple") } @@ -96,6 +249,8 @@ func (f TableFormat) String() string { return "(Pebble,v2)" case TableFormatPebblev3: return "(Pebble,v3)" + case TableFormatPebblev4: + return "(Pebble,v4)" default: panic("sstable: unknown table format version tuple") } diff --git a/vendor/github.com/cockroachdb/pebble/sstable/internal.go b/vendor/github.com/cockroachdb/pebble/sstable/internal.go index 9bab181..0fe7c99 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/internal.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/internal.go @@ -23,6 +23,8 @@ const ( InternalKeyKindLogData = base.InternalKeyKindLogData InternalKeyKindSingleDelete = base.InternalKeyKindSingleDelete InternalKeyKindRangeDelete = base.InternalKeyKindRangeDelete + InternalKeyKindSetWithDelete = base.InternalKeyKindSetWithDelete + InternalKeyKindDeleteSized = base.InternalKeyKindDeleteSized InternalKeyKindMax = base.InternalKeyKindMax InternalKeyKindInvalid = base.InternalKeyKindInvalid InternalKeySeqNumBatch = base.InternalKeySeqNumBatch diff --git a/vendor/github.com/cockroachdb/pebble/sstable/layout.go b/vendor/github.com/cockroachdb/pebble/sstable/layout.go new file mode 100644 index 0000000..bff1e30 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/sstable/layout.go @@ -0,0 +1,306 @@ +// Copyright 2011 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 sstable + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "sort" + "unsafe" + + "github.com/cockroachdb/pebble/internal/base" +) + +// Layout describes the block organization of an sstable. +type Layout struct { + // NOTE: changes to fields in this struct should also be reflected in + // ValidateBlockChecksums, which validates a static list of BlockHandles + // referenced in this struct. + + Data []BlockHandleWithProperties + Index []BlockHandle + TopIndex BlockHandle + Filter BlockHandle + RangeDel BlockHandle + RangeKey BlockHandle + ValueBlock []BlockHandle + ValueIndex BlockHandle + Properties BlockHandle + MetaIndex BlockHandle + Footer BlockHandle + Format TableFormat +} + +// Describe returns a description of the layout. If the verbose parameter is +// true, details of the structure of each block are returned as well. +func (l *Layout) Describe( + w io.Writer, verbose bool, r *Reader, fmtRecord func(key *base.InternalKey, value []byte), +) { + ctx := context.TODO() + type block struct { + BlockHandle + name string + } + var blocks []block + + for i := range l.Data { + blocks = append(blocks, block{l.Data[i].BlockHandle, "data"}) + } + for i := range l.Index { + blocks = append(blocks, block{l.Index[i], "index"}) + } + if l.TopIndex.Length != 0 { + blocks = append(blocks, block{l.TopIndex, "top-index"}) + } + if l.Filter.Length != 0 { + blocks = append(blocks, block{l.Filter, "filter"}) + } + if l.RangeDel.Length != 0 { + blocks = append(blocks, block{l.RangeDel, "range-del"}) + } + if l.RangeKey.Length != 0 { + blocks = append(blocks, block{l.RangeKey, "range-key"}) + } + for i := range l.ValueBlock { + blocks = append(blocks, block{l.ValueBlock[i], "value-block"}) + } + if l.ValueIndex.Length != 0 { + blocks = append(blocks, block{l.ValueIndex, "value-index"}) + } + if l.Properties.Length != 0 { + blocks = append(blocks, block{l.Properties, "properties"}) + } + if l.MetaIndex.Length != 0 { + blocks = append(blocks, block{l.MetaIndex, "meta-index"}) + } + if l.Footer.Length != 0 { + if l.Footer.Length == levelDBFooterLen { + blocks = append(blocks, block{l.Footer, "leveldb-footer"}) + } else { + blocks = append(blocks, block{l.Footer, "footer"}) + } + } + + sort.Slice(blocks, func(i, j int) bool { + return blocks[i].Offset < blocks[j].Offset + }) + + for i := range blocks { + b := &blocks[i] + fmt.Fprintf(w, "%10d %s (%d)\n", b.Offset, b.name, b.Length) + + if !verbose { + continue + } + if b.name == "filter" { + continue + } + + if b.name == "footer" || b.name == "leveldb-footer" { + trailer, offset := make([]byte, b.Length), b.Offset + _ = r.readable.ReadAt(ctx, trailer, int64(offset)) + + if b.name == "footer" { + checksumType := ChecksumType(trailer[0]) + fmt.Fprintf(w, "%10d checksum type: %s\n", offset, checksumType) + trailer, offset = trailer[1:], offset+1 + } + + metaHandle, n := binary.Uvarint(trailer) + metaLen, m := binary.Uvarint(trailer[n:]) + fmt.Fprintf(w, "%10d meta: offset=%d, length=%d\n", offset, metaHandle, metaLen) + trailer, offset = trailer[n+m:], offset+uint64(n+m) + + indexHandle, n := binary.Uvarint(trailer) + indexLen, m := binary.Uvarint(trailer[n:]) + fmt.Fprintf(w, "%10d index: offset=%d, length=%d\n", offset, indexHandle, indexLen) + trailer, offset = trailer[n+m:], offset+uint64(n+m) + + fmt.Fprintf(w, "%10d [padding]\n", offset) + + trailing := 12 + if b.name == "leveldb-footer" { + trailing = 8 + } + + offset += uint64(len(trailer) - trailing) + trailer = trailer[len(trailer)-trailing:] + + if b.name == "footer" { + version := trailer[:4] + fmt.Fprintf(w, "%10d version: %d\n", offset, binary.LittleEndian.Uint32(version)) + trailer, offset = trailer[4:], offset+4 + } + + magicNumber := trailer + fmt.Fprintf(w, "%10d magic number: 0x%x\n", offset, magicNumber) + + continue + } + + h, err := r.readBlock( + context.Background(), b.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* buffer pool */) + if err != nil { + fmt.Fprintf(w, " [err: %s]\n", err) + continue + } + + getRestart := func(data []byte, restarts, i int32) int32 { + return decodeRestart(data[restarts+4*i:]) + } + + formatIsRestart := func(data []byte, restarts, numRestarts, offset int32) { + i := sort.Search(int(numRestarts), func(i int) bool { + return getRestart(data, restarts, int32(i)) >= offset + }) + if i < int(numRestarts) && getRestart(data, restarts, int32(i)) == offset { + fmt.Fprintf(w, " [restart]\n") + } else { + fmt.Fprintf(w, "\n") + } + } + + formatRestarts := func(data []byte, restarts, numRestarts int32) { + for i := int32(0); i < numRestarts; i++ { + offset := getRestart(data, restarts, i) + fmt.Fprintf(w, "%10d [restart %d]\n", + b.Offset+uint64(restarts+4*i), b.Offset+uint64(offset)) + } + } + + formatTrailer := func() { + trailer := make([]byte, blockTrailerLen) + offset := int64(b.Offset + b.Length) + _ = r.readable.ReadAt(ctx, trailer, offset) + bt := blockType(trailer[0]) + checksum := binary.LittleEndian.Uint32(trailer[1:]) + fmt.Fprintf(w, "%10d [trailer compression=%s checksum=0x%04x]\n", offset, bt, checksum) + } + + var lastKey InternalKey + switch b.name { + case "data", "range-del", "range-key": + iter, _ := newBlockIter(r.Compare, h.Get()) + for key, value := iter.First(); key != nil; key, value = iter.Next() { + ptr := unsafe.Pointer(uintptr(iter.ptr) + uintptr(iter.offset)) + shared, ptr := decodeVarint(ptr) + unshared, ptr := decodeVarint(ptr) + value2, _ := decodeVarint(ptr) + + total := iter.nextOffset - iter.offset + // The format of the numbers in the record line is: + // + // ( = [] + + ) + // + // is the total number of bytes for the record. + // is the size of the 3 varint encoded integers for , + // , and . + // is the number of key bytes shared with the previous key. + // is the number of unshared key bytes. + // is the number of value bytes. + fmt.Fprintf(w, "%10d record (%d = %d [%d] + %d + %d)", + b.Offset+uint64(iter.offset), total, + total-int32(unshared+value2), shared, unshared, value2) + formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) + if fmtRecord != nil { + fmt.Fprintf(w, " ") + if l.Format < TableFormatPebblev3 { + fmtRecord(key, value.InPlaceValue()) + } else { + // InPlaceValue() will succeed even for data blocks where the + // actual value is in a different location, since this value was + // fetched from a blockIter which does not know about value + // blocks. + v := value.InPlaceValue() + if base.TrailerKind(key.Trailer) != InternalKeyKindSet { + fmtRecord(key, v) + } else if !isValueHandle(valuePrefix(v[0])) { + fmtRecord(key, v[1:]) + } else { + vh := decodeValueHandle(v[1:]) + fmtRecord(key, []byte(fmt.Sprintf("value handle %+v", vh))) + } + } + } + + if base.InternalCompare(r.Compare, lastKey, *key) >= 0 { + fmt.Fprintf(w, " WARNING: OUT OF ORDER KEYS!\n") + } + lastKey.Trailer = key.Trailer + lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...) + } + formatRestarts(iter.data, iter.restarts, iter.numRestarts) + formatTrailer() + case "index", "top-index": + iter, _ := newBlockIter(r.Compare, h.Get()) + for key, value := iter.First(); key != nil; key, value = iter.Next() { + bh, err := decodeBlockHandleWithProperties(value.InPlaceValue()) + if err != nil { + fmt.Fprintf(w, "%10d [err: %s]\n", b.Offset+uint64(iter.offset), err) + continue + } + fmt.Fprintf(w, "%10d block:%d/%d", + b.Offset+uint64(iter.offset), bh.Offset, bh.Length) + formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) + } + formatRestarts(iter.data, iter.restarts, iter.numRestarts) + formatTrailer() + case "properties": + iter, _ := newRawBlockIter(r.Compare, h.Get()) + for valid := iter.First(); valid; valid = iter.Next() { + fmt.Fprintf(w, "%10d %s (%d)", + b.Offset+uint64(iter.offset), iter.Key().UserKey, iter.nextOffset-iter.offset) + formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) + } + formatRestarts(iter.data, iter.restarts, iter.numRestarts) + formatTrailer() + case "meta-index": + iter, _ := newRawBlockIter(r.Compare, h.Get()) + for valid := iter.First(); valid; valid = iter.Next() { + value := iter.Value() + var bh BlockHandle + var n int + var vbih valueBlocksIndexHandle + isValueBlocksIndexHandle := false + if bytes.Equal(iter.Key().UserKey, []byte(metaValueIndexName)) { + vbih, n, err = decodeValueBlocksIndexHandle(value) + bh = vbih.h + isValueBlocksIndexHandle = true + } else { + bh, n = decodeBlockHandle(value) + } + if n == 0 || n != len(value) { + fmt.Fprintf(w, "%10d [err: %s]\n", b.Offset+uint64(iter.offset), err) + continue + } + var vbihStr string + if isValueBlocksIndexHandle { + vbihStr = fmt.Sprintf(" value-blocks-index-lengths: %d(num), %d(offset), %d(length)", + vbih.blockNumByteLength, vbih.blockOffsetByteLength, vbih.blockLengthByteLength) + } + fmt.Fprintf(w, "%10d %s block:%d/%d%s", + b.Offset+uint64(iter.offset), iter.Key().UserKey, + bh.Offset, bh.Length, vbihStr) + formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) + } + formatRestarts(iter.data, iter.restarts, iter.numRestarts) + formatTrailer() + case "value-block": + // We don't peer into the value-block since it can't be interpreted + // without the valueHandles. + case "value-index": + // We have already read the value-index to construct the list of + // value-blocks, so no need to do it again. + } + + h.Release() + } + + last := blocks[len(blocks)-1] + fmt.Fprintf(w, "%10d EOF\n", last.Offset+last.Length) +} diff --git a/vendor/github.com/cockroachdb/pebble/sstable/options.go b/vendor/github.com/cockroachdb/pebble/sstable/options.go index 66dbdd4..c5e1f79 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/options.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/options.go @@ -21,6 +21,17 @@ const ( NCompression ) +var ignoredInternalProperties = map[string]struct{}{ + "rocksdb.column.family.id": {}, + "rocksdb.fixed.key.length": {}, + "rocksdb.index.key.is.user.key": {}, + "rocksdb.index.value.is.delta.encoded": {}, + "rocksdb.oldest.key.time": {}, + "rocksdb.creation.time": {}, + "rocksdb.file.creation.time": {}, + "rocksdb.format.version": {}, +} + func (c Compression) String() string { switch c { case DefaultCompression: @@ -100,6 +111,9 @@ type ReaderOptions struct { // The default cache size is a zero-size cache. Cache *cache.Cache + // User properties specified in this map will not be added to sst.Properties.UserProperties. + DeniedUserProperties map[string]struct{} + // Comparer defines a total ordering over the space of []byte keys: a 'less // than' relationship. The same comparison algorithm must be used for reads // and writes over the lifetime of the DB. @@ -107,6 +121,9 @@ type ReaderOptions struct { // The default value uses the same ordering as bytes.Compare. Comparer *Comparer + // Merge defines the Merge function in use for this keyspace. + Merge base.Merge + // Filters is a map from filter policy name to filter policy. It is used for // debugging tools which may be used on multiple databases configured with // different filter policies. It is not necessary to populate this filters @@ -126,12 +143,18 @@ func (o ReaderOptions) ensureDefaults() ReaderOptions { if o.Comparer == nil { o.Comparer = base.DefaultComparer } + if o.Merge == nil { + o.Merge = base.DefaultMerger.Merge + } if o.MergerName == "" { o.MergerName = base.DefaultMerger.Name } if o.LoggerAndTracer == nil { o.LoggerAndTracer = base.NoopLoggerAndTracer{} } + if o.DeniedUserProperties == nil { + o.DeniedUserProperties = ignoredInternalProperties + } return o } @@ -210,6 +233,17 @@ type WriterOptions struct { // by a wider range of tools and libraries. TableFormat TableFormat + // IsStrictObsolete is only relevant for >= TableFormatPebblev4. See comment + // in format.go. Must be false if format < TableFormatPebblev4. + // + // TODO(bilal): set this when writing shared ssts. + IsStrictObsolete bool + + // WritingToLowestLevel is only relevant for >= TableFormatPebblev4. It is + // used to set the obsolete bit on DEL/DELSIZED/SINGLEDEL if they are the + // youngest for a userkey. + WritingToLowestLevel bool + // TablePropertyCollectors is a list of TablePropertyCollector creation // functions. A new TablePropertyCollector is created for each sstable built // and lives for the lifetime of the table. diff --git a/vendor/github.com/cockroachdb/pebble/sstable/properties.go b/vendor/github.com/cockroachdb/pebble/sstable/properties.go index 6be7f8d..3bbf34a 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/properties.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/properties.go @@ -23,20 +23,22 @@ var propTagMap = make(map[string]reflect.StructField) var propBoolTrue = []byte{'1'} var propBoolFalse = []byte{'0'} -var columnFamilyIDField = func() reflect.StructField { - f, ok := reflect.TypeOf(Properties{}).FieldByName("ColumnFamilyID") - if !ok { - panic("Properties.ColumnFamilyID field not found") - } - return f -}() - var propOffsetTagMap = make(map[uintptr]string) -func init() { - t := reflect.TypeOf(Properties{}) +func generateTagMaps(t reflect.Type, indexPrefix []int) { for i := 0; i < t.NumField(); i++ { f := t.Field(i) + if f.Type.Kind() == reflect.Struct { + if tag := f.Tag.Get("prop"); i == 0 && tag == "pebble.embbeded_common_properties" { + // CommonProperties struct embedded in Properties. Note that since + // CommonProperties is placed at the top of properties we can use + // the offsets of the fields within CommonProperties to determine + // the offsets of those fields within Properties. + generateTagMaps(f.Type, []int{i}) + continue + } + panic("pebble: unknown struct type in Properties") + } if tag := f.Tag.Get("prop"); tag != "" { switch f.Type.Kind() { case reflect.Bool: @@ -46,86 +48,125 @@ func init() { default: panic(fmt.Sprintf("unsupported property field type: %s %s", f.Name, f.Type)) } + if len(indexPrefix) > 0 { + // Prepend the index prefix so that we can use FieldByIndex on the top-level struct. + f.Index = append(indexPrefix[:len(indexPrefix):len(indexPrefix)], f.Index...) + } propTagMap[tag] = f propOffsetTagMap[f.Offset] = tag } } } -// Properties holds the sstable property values. The properties are -// automatically populated during sstable creation and load from the properties -// meta block when an sstable is opened. -type Properties struct { - // ID of column family for this SST file, corresponding to the CF identified - // by column_family_name. - ColumnFamilyID uint64 `prop:"rocksdb.column.family.id"` - // Name of the column family with which this SST file is associated. Empty if - // the column family is unknown. - ColumnFamilyName string `prop:"rocksdb.column.family.name"` - // The name of the comparer used in this table. - ComparerName string `prop:"rocksdb.comparator"` - // The compression algorithm used to compress blocks. - CompressionName string `prop:"rocksdb.compression"` - // The compression options used to compress blocks. - CompressionOptions string `prop:"rocksdb.compression_options"` - // The time when the SST file was created. Since SST files are immutable, - // this is equivalent to last modified time. - CreationTime uint64 `prop:"rocksdb.creation.time"` - // The total size of all data blocks. - DataSize uint64 `prop:"rocksdb.data.size"` - // The external sstable version format. Version 2 is the one RocksDB has been - // using since 5.13. RocksDB only uses the global sequence number for an - // sstable if this property has been set. - ExternalFormatVersion uint32 `prop:"rocksdb.external_sst_file.version"` - // Actual SST file creation time. 0 means unknown. - FileCreationTime uint64 `prop:"rocksdb.file.creation.time"` - // The name of the filter policy used in this table. Empty if no filter - // policy is used. - FilterPolicyName string `prop:"rocksdb.filter.policy"` - // The size of filter block. - FilterSize uint64 `prop:"rocksdb.filter.size"` - // If 0, key is variable length. Otherwise number of bytes for each key. - FixedKeyLen uint64 `prop:"rocksdb.fixed.key.length"` - // Format version, reserved for backward compatibility. - FormatVersion uint64 `prop:"rocksdb.format.version"` - // The global sequence number to use for all entries in the table. Present if - // the table was created externally and ingested whole. - GlobalSeqNum uint64 `prop:"rocksdb.external_sst_file.global_seqno"` - // Whether the index key is user key or an internal key. - IndexKeyIsUserKey uint64 `prop:"rocksdb.index.key.is.user.key"` - // Total number of index partitions if kTwoLevelIndexSearch is used. - IndexPartitions uint64 `prop:"rocksdb.index.partitions"` - // The size of index block. - IndexSize uint64 `prop:"rocksdb.index.size"` - // The index type. TODO(peter): add a more detailed description. - IndexType uint32 `prop:"rocksdb.block.based.table.index.type"` - // Whether delta encoding is used to encode the index values. - IndexValueIsDeltaEncoded uint64 `prop:"rocksdb.index.value.is.delta.encoded"` - // The name of the merger used in this table. Empty if no merger is used. - MergerName string `prop:"rocksdb.merge.operator"` - // The number of blocks in this table. - NumDataBlocks uint64 `prop:"rocksdb.num.data.blocks"` +func init() { + generateTagMaps(reflect.TypeOf(Properties{}), nil) +} + +// CommonProperties holds properties for either a virtual or a physical sstable. This +// can be used by code which doesn't care to make the distinction between physical +// and virtual sstables properties. +// +// For virtual sstables, fields are constructed through extrapolation upon virtual +// reader construction. See MakeVirtualReader for implementation details. +// +// NB: The values of these properties can affect correctness. For example, +// if NumRangeKeySets == 0, but the sstable actually contains range keys, then +// the iterators will behave incorrectly. +type CommonProperties struct { + // The number of entries in this table. + NumEntries uint64 `prop:"rocksdb.num.entries"` + // Total raw key size. + RawKeySize uint64 `prop:"rocksdb.raw.key.size"` + // Total raw value size. + RawValueSize uint64 `prop:"rocksdb.raw.value.size"` + // Total raw key size of point deletion tombstones. This value is comparable + // to RawKeySize. + RawPointTombstoneKeySize uint64 `prop:"pebble.raw.point-tombstone.key.size"` + // Sum of the raw value sizes carried by point deletion tombstones + // containing size estimates. See the DeleteSized key kind. This value is + // comparable to Raw{Key,Value}Size. + RawPointTombstoneValueSize uint64 `prop:"pebble.raw.point-tombstone.value.size"` + // The number of point deletion entries ("tombstones") in this table that + // carry a size hint indicating the size of the value the tombstone deletes. + NumSizedDeletions uint64 `prop:"pebble.num.deletions.sized"` // The number of deletion entries in this table, including both point and // range deletions. NumDeletions uint64 `prop:"rocksdb.deleted.keys"` - // The number of entries in this table. - NumEntries uint64 `prop:"rocksdb.num.entries"` - // The number of merge operands in the table. - NumMergeOperands uint64 `prop:"rocksdb.merge.operands"` // The number of range deletions in this table. NumRangeDeletions uint64 `prop:"rocksdb.num.range-deletions"` // The number of RANGEKEYDELs in this table. NumRangeKeyDels uint64 `prop:"pebble.num.range-key-dels"` // The number of RANGEKEYSETs in this table. NumRangeKeySets uint64 `prop:"pebble.num.range-key-sets"` + // Total size of value blocks and value index block. Only serialized if > 0. + ValueBlocksSize uint64 `prop:"pebble.value-blocks.size"` +} + +// String is only used for testing purposes. +func (c *CommonProperties) String() string { + var buf bytes.Buffer + v := reflect.ValueOf(*c) + loaded := make(map[uintptr]struct{}) + writeProperties(loaded, v, &buf) + return buf.String() +} + +// NumPointDeletions is the number of point deletions in the sstable. For virtual +// sstables, this is an estimate. +func (c *CommonProperties) NumPointDeletions() uint64 { + return c.NumDeletions - c.NumRangeDeletions +} + +// Properties holds the sstable property values. The properties are +// automatically populated during sstable creation and load from the properties +// meta block when an sstable is opened. +type Properties struct { + // CommonProperties needs to be at the top of the Properties struct so that the + // offsets of the fields in CommonProperties match the offsets of the embedded + // fields of CommonProperties in Properties. + CommonProperties `prop:"pebble.embbeded_common_properties"` + + // The name of the comparer used in this table. + ComparerName string `prop:"rocksdb.comparator"` + // The compression algorithm used to compress blocks. + CompressionName string `prop:"rocksdb.compression"` + // The compression options used to compress blocks. + CompressionOptions string `prop:"rocksdb.compression_options"` + // The total size of all data blocks. + DataSize uint64 `prop:"rocksdb.data.size"` + // The external sstable version format. Version 2 is the one RocksDB has been + // using since 5.13. RocksDB only uses the global sequence number for an + // sstable if this property has been set. + ExternalFormatVersion uint32 `prop:"rocksdb.external_sst_file.version"` + // The name of the filter policy used in this table. Empty if no filter + // policy is used. + FilterPolicyName string `prop:"rocksdb.filter.policy"` + // The size of filter block. + FilterSize uint64 `prop:"rocksdb.filter.size"` + // The global sequence number to use for all entries in the table. Present if + // the table was created externally and ingested whole. + GlobalSeqNum uint64 `prop:"rocksdb.external_sst_file.global_seqno"` + // Total number of index partitions if kTwoLevelIndexSearch is used. + IndexPartitions uint64 `prop:"rocksdb.index.partitions"` + // The size of index block. + IndexSize uint64 `prop:"rocksdb.index.size"` + // The index type. TODO(peter): add a more detailed description. + IndexType uint32 `prop:"rocksdb.block.based.table.index.type"` + // For formats >= TableFormatPebblev4, this is set to true if the obsolete + // bit is strict for all the point keys. + IsStrictObsolete bool `prop:"pebble.obsolete.is_strict"` + // The name of the merger used in this table. Empty if no merger is used. + MergerName string `prop:"rocksdb.merge.operator"` + // The number of blocks in this table. + NumDataBlocks uint64 `prop:"rocksdb.num.data.blocks"` + // The number of merge operands in the table. + NumMergeOperands uint64 `prop:"rocksdb.merge.operands"` // The number of RANGEKEYUNSETs in this table. NumRangeKeyUnsets uint64 `prop:"pebble.num.range-key-unsets"` // The number of value blocks in this table. Only serialized if > 0. NumValueBlocks uint64 `prop:"pebble.num.value-blocks"` // The number of values stored in value blocks. Only serialized if > 0. NumValuesInValueBlocks uint64 `prop:"pebble.num.values.in.value-blocks"` - // Timestamp of the earliest key. 0 if unknown. - OldestKeyTime uint64 `prop:"rocksdb.oldest.key.time"` // The name of the prefix extractor used in this table. Empty if no prefix // extractor is used. PrefixExtractorName string `prop:"rocksdb.prefix.extractor.name"` @@ -134,14 +175,10 @@ type Properties struct { // A comma separated list of names of the property collectors used in this // table. PropertyCollectorNames string `prop:"rocksdb.property.collectors"` - // Total raw key size. - RawKeySize uint64 `prop:"rocksdb.raw.key.size"` // Total raw rangekey key size. RawRangeKeyKeySize uint64 `prop:"pebble.raw.range-key.key.size"` // Total raw rangekey value size. RawRangeKeyValueSize uint64 `prop:"pebble.raw.range-key.value.size"` - // Total raw value size. - RawValueSize uint64 `prop:"rocksdb.raw.value.size"` // The total number of keys in this table that were pinned by open snapshots. SnapshotPinnedKeys uint64 `prop:"pebble.num.snapshot-pinned-keys"` // The cumulative bytes of keys in this table that were pinned by @@ -154,8 +191,6 @@ type Properties struct { TopLevelIndexSize uint64 `prop:"rocksdb.top-level.index.size"` // User collected properties. UserProperties map[string]string - // Total size of value blocks and value index block. Only serialized if > 0. - ValueBlocksSize uint64 `prop:"pebble.value-blocks.size"` // If filtering is enabled, was the filter created on the whole key. WholeKeyFiltering bool `prop:"rocksdb.block.based.table.whole.key.filtering"` @@ -176,12 +211,15 @@ func (p *Properties) NumRangeKeys() uint64 { return p.NumRangeKeyDels + p.NumRangeKeySets + p.NumRangeKeyUnsets } -func (p *Properties) String() string { - var buf bytes.Buffer - v := reflect.ValueOf(*p) +func writeProperties(loaded map[uintptr]struct{}, v reflect.Value, buf *bytes.Buffer) { vt := v.Type() for i := 0; i < v.NumField(); i++ { ft := vt.Field(i) + if ft.Type.Kind() == reflect.Struct { + // Embedded struct within the properties. + writeProperties(loaded, v.Field(i), buf) + continue + } tag := ft.Tag.Get("prop") if tag == "" { continue @@ -191,30 +229,33 @@ func (p *Properties) String() string { // TODO(peter): Use f.IsZero() when we can rely on go1.13. if zero := reflect.Zero(f.Type()); zero.Interface() == f.Interface() { // Skip printing of zero values which were not loaded from disk. - if _, ok := p.Loaded[ft.Offset]; !ok { + if _, ok := loaded[ft.Offset]; !ok { continue } } - fmt.Fprintf(&buf, "%s: ", tag) + fmt.Fprintf(buf, "%s: ", tag) switch ft.Type.Kind() { case reflect.Bool: - fmt.Fprintf(&buf, "%t\n", f.Bool()) + fmt.Fprintf(buf, "%t\n", f.Bool()) case reflect.Uint32: - fmt.Fprintf(&buf, "%d\n", f.Uint()) + fmt.Fprintf(buf, "%d\n", f.Uint()) case reflect.Uint64: - u := f.Uint() - if ft.Offset == columnFamilyIDField.Offset && u == math.MaxInt32 { - fmt.Fprintf(&buf, "-\n") - } else { - fmt.Fprintf(&buf, "%d\n", f.Uint()) - } + fmt.Fprintf(buf, "%d\n", f.Uint()) case reflect.String: - fmt.Fprintf(&buf, "%s\n", f.String()) + fmt.Fprintf(buf, "%s\n", f.String()) default: panic("not reached") } } +} + +func (p *Properties) String() string { + var buf bytes.Buffer + v := reflect.ValueOf(*p) + writeProperties(p.Loaded, v, &buf) + + // Write the UserProperties. keys := make([]string, 0, len(p.UserProperties)) for key := range p.UserProperties { keys = append(keys, key) @@ -226,16 +267,18 @@ func (p *Properties) String() string { return buf.String() } -func (p *Properties) load(b block, blockOffset uint64) error { +func (p *Properties) load( + b block, blockOffset uint64, deniedUserProperties map[string]struct{}, +) error { i, err := newRawBlockIter(bytes.Compare, b) if err != nil { return err } p.Loaded = make(map[uintptr]struct{}) v := reflect.ValueOf(p).Elem() + for valid := i.First(); valid; valid = i.Next() { - tag := intern.Bytes(i.Key().UserKey) - if f, ok := propTagMap[tag]; ok { + if f, ok := propTagMap[string(i.Key().UserKey)]; ok { p.Loaded[f.Offset] = struct{}{} field := v.FieldByIndex(f.Index) switch f.Type.Kind() { @@ -245,7 +288,7 @@ func (p *Properties) load(b block, blockOffset uint64) error { field.SetUint(uint64(binary.LittleEndian.Uint32(i.Value()))) case reflect.Uint64: var n uint64 - if tag == propGlobalSeqnumName { + if string(i.Key().UserKey) == propGlobalSeqnumName { n = binary.LittleEndian.Uint64(i.Value()) } else { n, _ = binary.Uvarint(i.Value()) @@ -261,7 +304,10 @@ func (p *Properties) load(b block, blockOffset uint64) error { if p.UserProperties == nil { p.UserProperties = make(map[string]string) } - p.UserProperties[tag] = string(i.Value()) + + if _, denied := deniedUserProperties[string(i.Key().UserKey)]; !denied { + p.UserProperties[intern.Bytes(i.Key().UserKey)] = string(i.Value()) + } } return nil } @@ -297,16 +343,12 @@ func (p *Properties) saveString(m map[string][]byte, offset uintptr, value strin m[propOffsetTagMap[offset]] = []byte(value) } -func (p *Properties) save(w *rawBlockWriter) { +func (p *Properties) save(tblFormat TableFormat, w *rawBlockWriter) { m := make(map[string][]byte) for k, v := range p.UserProperties { m[k] = []byte(v) } - p.saveUvarint(m, unsafe.Offsetof(p.ColumnFamilyID), p.ColumnFamilyID) - if p.ColumnFamilyName != "" { - p.saveString(m, unsafe.Offsetof(p.ColumnFamilyName), p.ColumnFamilyName) - } if p.ComparerName != "" { p.saveString(m, unsafe.Offsetof(p.ComparerName), p.ComparerName) } @@ -316,37 +358,45 @@ func (p *Properties) save(w *rawBlockWriter) { if p.CompressionOptions != "" { p.saveString(m, unsafe.Offsetof(p.CompressionOptions), p.CompressionOptions) } - p.saveUvarint(m, unsafe.Offsetof(p.CreationTime), p.CreationTime) p.saveUvarint(m, unsafe.Offsetof(p.DataSize), p.DataSize) if p.ExternalFormatVersion != 0 { p.saveUint32(m, unsafe.Offsetof(p.ExternalFormatVersion), p.ExternalFormatVersion) p.saveUint64(m, unsafe.Offsetof(p.GlobalSeqNum), p.GlobalSeqNum) } - if p.FileCreationTime > 0 { - p.saveUvarint(m, unsafe.Offsetof(p.FileCreationTime), p.FileCreationTime) - } if p.FilterPolicyName != "" { p.saveString(m, unsafe.Offsetof(p.FilterPolicyName), p.FilterPolicyName) } p.saveUvarint(m, unsafe.Offsetof(p.FilterSize), p.FilterSize) - p.saveUvarint(m, unsafe.Offsetof(p.FixedKeyLen), p.FixedKeyLen) - p.saveUvarint(m, unsafe.Offsetof(p.FormatVersion), p.FormatVersion) - p.saveUvarint(m, unsafe.Offsetof(p.IndexKeyIsUserKey), p.IndexKeyIsUserKey) if p.IndexPartitions != 0 { p.saveUvarint(m, unsafe.Offsetof(p.IndexPartitions), p.IndexPartitions) p.saveUvarint(m, unsafe.Offsetof(p.TopLevelIndexSize), p.TopLevelIndexSize) } p.saveUvarint(m, unsafe.Offsetof(p.IndexSize), p.IndexSize) p.saveUint32(m, unsafe.Offsetof(p.IndexType), p.IndexType) - p.saveUvarint(m, unsafe.Offsetof(p.IndexValueIsDeltaEncoded), p.IndexValueIsDeltaEncoded) + if p.IsStrictObsolete { + p.saveBool(m, unsafe.Offsetof(p.IsStrictObsolete), p.IsStrictObsolete) + } if p.MergerName != "" { p.saveString(m, unsafe.Offsetof(p.MergerName), p.MergerName) } p.saveUvarint(m, unsafe.Offsetof(p.NumDataBlocks), p.NumDataBlocks) p.saveUvarint(m, unsafe.Offsetof(p.NumEntries), p.NumEntries) p.saveUvarint(m, unsafe.Offsetof(p.NumDeletions), p.NumDeletions) + if p.NumSizedDeletions > 0 { + p.saveUvarint(m, unsafe.Offsetof(p.NumSizedDeletions), p.NumSizedDeletions) + } p.saveUvarint(m, unsafe.Offsetof(p.NumMergeOperands), p.NumMergeOperands) p.saveUvarint(m, unsafe.Offsetof(p.NumRangeDeletions), p.NumRangeDeletions) + // NB: We only write out some properties for Pebble formats. This isn't + // strictly necessary because unrecognized properties are interpreted as + // user-defined properties, however writing them prevents byte-for-byte + // equivalence with RocksDB files that some of our testing requires. + if p.RawPointTombstoneKeySize > 0 && tblFormat >= TableFormatPebblev1 { + p.saveUvarint(m, unsafe.Offsetof(p.RawPointTombstoneKeySize), p.RawPointTombstoneKeySize) + } + if p.RawPointTombstoneValueSize > 0 { + p.saveUvarint(m, unsafe.Offsetof(p.RawPointTombstoneValueSize), p.RawPointTombstoneValueSize) + } if p.NumRangeKeys() > 0 { p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeyDels), p.NumRangeKeyDels) p.saveUvarint(m, unsafe.Offsetof(p.NumRangeKeySets), p.NumRangeKeySets) @@ -360,7 +410,6 @@ func (p *Properties) save(w *rawBlockWriter) { if p.NumValuesInValueBlocks > 0 { p.saveUvarint(m, unsafe.Offsetof(p.NumValuesInValueBlocks), p.NumValuesInValueBlocks) } - p.saveUvarint(m, unsafe.Offsetof(p.OldestKeyTime), p.OldestKeyTime) if p.PrefixExtractorName != "" { p.saveString(m, unsafe.Offsetof(p.PrefixExtractorName), p.PrefixExtractorName) } @@ -380,6 +429,16 @@ func (p *Properties) save(w *rawBlockWriter) { } p.saveBool(m, unsafe.Offsetof(p.WholeKeyFiltering), p.WholeKeyFiltering) + if tblFormat < TableFormatPebblev1 { + m["rocksdb.column.family.id"] = binary.AppendUvarint([]byte(nil), math.MaxInt32) + m["rocksdb.fixed.key.length"] = []byte{0x00} + m["rocksdb.index.key.is.user.key"] = []byte{0x00} + m["rocksdb.index.value.is.delta.encoded"] = []byte{0x00} + m["rocksdb.oldest.key.time"] = []byte{0x00} + m["rocksdb.creation.time"] = []byte{0x00} + m["rocksdb.format.version"] = []byte{0x00} + } + keys := make([]string, 0, len(m)) for key := range m { keys = append(keys, key) diff --git a/vendor/github.com/cockroachdb/pebble/sstable/reader.go b/vendor/github.com/cockroachdb/pebble/sstable/reader.go index 3c9aa38..d86a424 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/reader.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/reader.go @@ -8,13 +8,10 @@ import ( "bytes" "context" "encoding/binary" - "fmt" "io" "os" "sort" - "sync" "time" - "unsafe" "github.com/cespare/xxhash/v2" "github.com/cockroachdb/errors" @@ -26,6 +23,7 @@ import ( "github.com/cockroachdb/pebble/internal/keyspan" "github.com/cockroachdb/pebble/internal/private" "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" ) var errCorruptIndexEntry = base.CorruptionErrorf("pebble/table: corrupt index entry") @@ -78,408 +76,6 @@ func encodeBlockHandleWithProperties(dst []byte, b BlockHandleWithProperties) [] // over those pairs. type block []byte -// Iterator iterates over an entire table of data. -type Iterator interface { - base.InternalIterator - - // NextPrefix implements (base.InternalIterator).NextPrefix. - NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) - - // MaybeFilteredKeys may be called when an iterator is exhausted to indicate - // whether or not the last positioning method may have skipped any keys due - // to block-property filters. This is used by the Pebble levelIter to - // control when an iterator steps to the next sstable. - // - // MaybeFilteredKeys may always return false positives, that is it may - // return true when no keys were filtered. It should only be called when the - // iterator is exhausted. It must never return false negatives when the - // iterator is exhausted. - MaybeFilteredKeys() bool - - SetCloseHook(fn func(i Iterator) error) -} - -// Iterator positioning optimizations and singleLevelIterator and -// twoLevelIterator: -// -// An iterator is absolute positioned using one of the Seek or First or Last -// calls. After absolute positioning, there can be relative positioning done -// by stepping using Prev or Next. -// -// We implement optimizations below where an absolute positioning call can in -// some cases use the current position to do less work. To understand these, -// we first define some terms. An iterator is bounds-exhausted if the bounds -// (upper of lower) have been reached. An iterator is data-exhausted if it has -// the reached the end of the data (forward or reverse) in the sstable. A -// singleLevelIterator only knows a local-data-exhausted property since when -// it is used as part of a twoLevelIterator, the twoLevelIterator can step to -// the next lower-level index block. -// -// The bounds-exhausted property is tracked by -// singleLevelIterator.exhaustedBounds being +1 (upper bound reached) or -1 -// (lower bound reached). The same field is reused by twoLevelIterator. Either -// may notice the exhaustion of the bound and set it. Note that if -// singleLevelIterator sets this property, it is not a local property (since -// the bound has been reached regardless of whether this is in the context of -// the twoLevelIterator or not). -// -// The data-exhausted property is tracked in a more subtle manner. We define -// two predicates: -// - partial-local-data-exhausted (PLDE): -// i.data.isDataInvalidated() || !i.data.valid() -// - partial-global-data-exhausted (PGDE): -// i.index.isDataInvalidated() || !i.index.valid() || i.data.isDataInvalidated() || -// !i.data.valid() -// -// PLDE is defined for a singleLevelIterator. PGDE is defined for a -// twoLevelIterator. Oddly, in our code below the singleLevelIterator does not -// know when it is part of a twoLevelIterator so it does not know when its -// property is local or global. -// -// Now to define data-exhausted: -// - Prerequisite: we must know that the iterator has been positioned and -// i.err is nil. -// - bounds-exhausted must not be true: -// If bounds-exhausted is true, we have incomplete knowledge of -// data-exhausted since PLDE or PGDE could be true because we could have -// chosen not to load index block or data block and figured out that the -// bound is exhausted (due to block property filters filtering out index and -// data blocks and going past the bound on the top level index block). Note -// that if we tried to separate out the BPF case from others we could -// develop more knowledge here. -// - PGDE is true for twoLevelIterator. PLDE is true if it is a standalone -// singleLevelIterator. !PLDE or !PGDE of course imply that data-exhausted -// is not true. -// -// An implication of the above is that if we are going to somehow utilize -// knowledge of data-exhausted in an optimization, we must not forget the -// existing value of bounds-exhausted since by forgetting the latter we can -// erroneously think that data-exhausted is true. Bug #2036 was due to this -// forgetting. -// -// Now to the two categories of optimizations we currently have: -// - Monotonic bounds optimization that reuse prior iterator position when -// doing seek: These only work with !data-exhausted. We could choose to make -// these work with data-exhausted but have not bothered because in the -// context of a DB if data-exhausted were true, the DB would move to the -// next file in the level. Note that this behavior of moving to the next -// file is not necessarily true for L0 files, so there could be some benefit -// in the future in this optimization. See the WARNING-data-exhausted -// comments if trying to optimize this in the future. -// - TrySeekUsingNext optimizations: these work regardless of exhaustion -// state. -// -// Implementation detail: In the code PLDE only checks that -// i.data.isDataInvalidated(). This narrower check is safe, since this is a -// subset of the set expressed by the OR expression. Also, it is not a -// de-optimization since whenever we exhaust the iterator we explicitly call -// i.data.invalidate(). PGDE checks i.index.isDataInvalidated() && -// i.data.isDataInvalidated(). Again, this narrower check is safe, and not a -// de-optimization since whenever we exhaust the iterator we explicitly call -// i.index.invalidate() and i.data.invalidate(). The && is questionable -- for -// now this is a bit of defensive code. We should seriously consider removing -// it, since defensive code suggests we are not confident about our invariants -// (and if we are not confident, we need more invariant assertions, not -// defensive code). -// -// TODO(sumeer): remove the aforementioned defensive code. - -// singleLevelIterator iterates over an entire table of data. To seek for a given -// key, it first looks in the index for the block that contains that key, and then -// looks inside that block. -type singleLevelIterator struct { - ctx context.Context - cmp Compare - // Global lower/upper bound for the iterator. - lower []byte - upper []byte - bpfs *BlockPropertiesFilterer - // Per-block lower/upper bound. Nil if the bound does not apply to the block - // because we determined the block lies completely within the bound. - blockLower []byte - blockUpper []byte - reader *Reader - index blockIter - data blockIter - dataRH objstorage.ReadHandle - dataRHPrealloc objstorage.PreallocatedReadHandle - // dataBH refers to the last data block that the iterator considered - // loading. It may not actually have loaded the block, due to an error or - // because it was considered irrelevant. - dataBH BlockHandle - vbReader *valueBlockReader - // vbRH is the read handle for value blocks, which are in a different - // part of the sstable than data blocks. - vbRH objstorage.ReadHandle - vbRHPrealloc objstorage.PreallocatedReadHandle - err error - closeHook func(i Iterator) error - stats *base.InternalIteratorStats - - // boundsCmp and positionedUsingLatestBounds are for optimizing iteration - // that uses multiple adjacent bounds. The seek after setting a new bound - // can use the fact that the iterator is either within the previous bounds - // or exactly one key before or after the bounds. If the new bounds is - // after/before the previous bounds, and we are already positioned at a - // block that is relevant for the new bounds, we can try to first position - // using Next/Prev (repeatedly) instead of doing a more expensive seek. - // - // When there are wide files at higher levels that match the bounds - // but don't have any data for the bound, we will already be - // positioned at the key beyond the bounds and won't need to do much - // work -- given that most data is in L6, such files are likely to - // dominate the performance of the mergingIter, and may be the main - // benefit of this performance optimization (of course it also helps - // when the file that has the data has successive seeks that stay in - // the same block). - // - // Specifically, boundsCmp captures the relationship between the previous - // and current bounds, if the iterator had been positioned after setting - // the previous bounds. If it was not positioned, i.e., Seek/First/Last - // were not called, we don't know where it is positioned and cannot - // optimize. - // - // Example: Bounds moving forward, and iterator exhausted in forward direction. - // bounds = [f, h), ^ shows block iterator position - // file contents [ a b c d e f g h i j k ] - // ^ - // new bounds = [j, k). Since positionedUsingLatestBounds=true, boundsCmp is - // set to +1. SeekGE(j) can use next (the optimization also requires that j - // is within the block, but that is not for correctness, but to limit the - // optimization to when it will actually be an optimization). - // - // Example: Bounds moving forward. - // bounds = [f, h), ^ shows block iterator position - // file contents [ a b c d e f g h i j k ] - // ^ - // new bounds = [j, k). Since positionedUsingLatestBounds=true, boundsCmp is - // set to +1. SeekGE(j) can use next. - // - // Example: Bounds moving forward, but iterator not positioned using previous - // bounds. - // bounds = [f, h), ^ shows block iterator position - // file contents [ a b c d e f g h i j k ] - // ^ - // new bounds = [i, j). Iterator is at j since it was never positioned using - // [f, h). So positionedUsingLatestBounds=false, and boundsCmp is set to 0. - // SeekGE(i) will not use next. - // - // Example: Bounds moving forward and sparse file - // bounds = [f, h), ^ shows block iterator position - // file contents [ a z ] - // ^ - // new bounds = [j, k). Since positionedUsingLatestBounds=true, boundsCmp is - // set to +1. SeekGE(j) notices that the iterator is already past j and does - // not need to do anything. - // - // Similar examples can be constructed for backward iteration. - // - // This notion of exactly one key before or after the bounds is not quite - // true when block properties are used to ignore blocks. In that case we - // can't stop precisely at the first block that is past the bounds since - // we are using the index entries to enforce the bounds. - // - // e.g. 3 blocks with keys [b, c] [f, g], [i, j, k] with index entries d, - // h, l. And let the lower bound be k, and we are reverse iterating. If - // the block [i, j, k] is ignored due to the block interval annotations we - // do need to move the index to block [f, g] since the index entry for the - // [i, j, k] block is l which is not less than the lower bound of k. So we - // have passed the entries i, j. - // - // This behavior is harmless since the block property filters are fixed - // for the lifetime of the iterator so i, j are irrelevant. In addition, - // the current code will not load the [f, g] block, so the seek - // optimization that attempts to use Next/Prev do not apply anyway. - boundsCmp int - positionedUsingLatestBounds bool - - // exhaustedBounds represents whether the iterator is exhausted for - // iteration by reaching the upper or lower bound. +1 when exhausted - // the upper bound, -1 when exhausted the lower bound, and 0 when - // neither. exhaustedBounds is also used for the TrySeekUsingNext - // optimization in twoLevelIterator and singleLevelIterator. Care should be - // taken in setting this in twoLevelIterator before calling into - // singleLevelIterator, given that these two iterators share this field. - exhaustedBounds int8 - - // maybeFilteredKeysSingleLevel indicates whether the last iterator - // positioning operation may have skipped any data blocks due to - // block-property filters when positioning the index. - maybeFilteredKeysSingleLevel bool - - // useFilter specifies whether the filter block in this sstable, if present, - // should be used for prefix seeks or not. In some cases it is beneficial - // to skip a filter block even if it exists (eg. if probability of a match - // is high). - useFilter bool - lastBloomFilterMatched bool -} - -// singleLevelIterator implements the base.InternalIterator interface. -var _ base.InternalIterator = (*singleLevelIterator)(nil) - -var singleLevelIterPool = sync.Pool{ - New: func() interface{} { - i := &singleLevelIterator{} - // Note: this is a no-op if invariants are disabled or race is enabled. - invariants.SetFinalizer(i, checkSingleLevelIterator) - return i - }, -} - -var twoLevelIterPool = sync.Pool{ - New: func() interface{} { - i := &twoLevelIterator{} - // Note: this is a no-op if invariants are disabled or race is enabled. - invariants.SetFinalizer(i, checkTwoLevelIterator) - return i - }, -} - -// TODO(jackson): rangedel fragmentBlockIters can't be pooled because of some -// code paths that double Close the iters. Fix the double close and pool the -// *fragmentBlockIter type directly. - -var rangeKeyFragmentBlockIterPool = sync.Pool{ - New: func() interface{} { - i := &rangeKeyFragmentBlockIter{} - // Note: this is a no-op if invariants are disabled or race is enabled. - invariants.SetFinalizer(i, checkRangeKeyFragmentBlockIterator) - return i - }, -} - -func checkSingleLevelIterator(obj interface{}) { - i := obj.(*singleLevelIterator) - if p := i.data.cacheHandle.Get(); p != nil { - fmt.Fprintf(os.Stderr, "singleLevelIterator.data.cacheHandle is not nil: %p\n", p) - os.Exit(1) - } - if p := i.index.cacheHandle.Get(); p != nil { - fmt.Fprintf(os.Stderr, "singleLevelIterator.index.cacheHandle is not nil: %p\n", p) - os.Exit(1) - } -} - -func checkTwoLevelIterator(obj interface{}) { - i := obj.(*twoLevelIterator) - if p := i.data.cacheHandle.Get(); p != nil { - fmt.Fprintf(os.Stderr, "singleLevelIterator.data.cacheHandle is not nil: %p\n", p) - os.Exit(1) - } - if p := i.index.cacheHandle.Get(); p != nil { - fmt.Fprintf(os.Stderr, "singleLevelIterator.index.cacheHandle is not nil: %p\n", p) - os.Exit(1) - } -} - -func checkRangeKeyFragmentBlockIterator(obj interface{}) { - i := obj.(*rangeKeyFragmentBlockIter) - if p := i.blockIter.cacheHandle.Get(); p != nil { - fmt.Fprintf(os.Stderr, "fragmentBlockIter.blockIter.cacheHandle is not nil: %p\n", p) - os.Exit(1) - } -} - -// init initializes a singleLevelIterator for reading from the table. It is -// synonmous with Reader.NewIter, but allows for reusing of the iterator -// between different Readers. -func (i *singleLevelIterator) init( - ctx context.Context, - r *Reader, - lower, upper []byte, - filterer *BlockPropertiesFilterer, - useFilter bool, - stats *base.InternalIteratorStats, - rp ReaderProvider, -) error { - if r.err != nil { - return r.err - } - indexH, err := r.readIndex(ctx, stats) - if err != nil { - return err - } - - i.ctx = ctx - i.lower = lower - i.upper = upper - i.bpfs = filterer - i.useFilter = useFilter - i.reader = r - i.cmp = r.Compare - i.stats = stats - err = i.index.initHandle(i.cmp, indexH, r.Properties.GlobalSeqNum) - if err != nil { - // blockIter.Close releases indexH and always returns a nil error - _ = i.index.Close() - return err - } - i.dataRH = objstorage.UsePreallocatedReadHandle(r.readable, &i.dataRHPrealloc) - if r.tableFormat == TableFormatPebblev3 { - if r.Properties.NumValueBlocks > 0 { - // NB: we cannot avoid this ~248 byte allocation, since valueBlockReader - // can outlive the singleLevelIterator due to be being embedded in a - // LazyValue. This consumes ~2% in microbenchmark CPU profiles, but we - // should only optimize this if it shows up as significant in end-to-end - // CockroachDB benchmarks, since it is tricky to do so. One possibility - // is that if many sstable iterators only get positioned at latest - // versions of keys, and therefore never expose a LazyValue that is - // separated to their callers, they can put this valueBlockReader into a - // sync.Pool. - i.vbReader = &valueBlockReader{ - ctx: ctx, - bpOpen: i, - rp: rp, - vbih: r.valueBIH, - stats: stats, - } - i.data.lazyValueHandling.vbr = i.vbReader - i.vbRH = objstorage.UsePreallocatedReadHandle(r.readable, &i.vbRHPrealloc) - } - i.data.lazyValueHandling.hasValuePrefix = true - } - return nil -} - -// setupForCompaction sets up the singleLevelIterator for use with compactionIter. -// Currently, it skips readahead ramp-up. It should be called after init is called. -func (i *singleLevelIterator) setupForCompaction() { - i.dataRH.MaxReadahead() - if i.vbRH != nil { - i.vbRH.MaxReadahead() - } -} - -func (i *singleLevelIterator) resetForReuse() singleLevelIterator { - return singleLevelIterator{ - index: i.index.resetForReuse(), - data: i.data.resetForReuse(), - } -} - -func (i *singleLevelIterator) initBounds() { - // Trim the iteration bounds for the current block. We don't have to check - // the bounds on each iteration if the block is entirely contained within the - // iteration bounds. - i.blockLower = i.lower - if i.blockLower != nil { - key, _ := i.data.First() - if key != nil && i.cmp(i.blockLower, key.UserKey) < 0 { - // The lower-bound is less than the first key in the block. No need - // to check the lower-bound again for this block. - i.blockLower = nil - } - } - i.blockUpper = i.upper - if i.blockUpper != nil && i.cmp(i.blockUpper, i.index.Key().UserKey) > 0 { - // The upper-bound is greater than the index key which itself is greater - // than or equal to every key in the block. No need to check the - // upper-bound again for this block. - i.blockUpper = nil - } -} - type loadBlockResult int8 const ( @@ -489,2024 +85,6 @@ const ( loadBlockIrrelevant ) -// loadBlock loads the block at the current index position and leaves i.data -// unpositioned. If unsuccessful, it sets i.err to any error encountered, which -// may be nil if we have simply exhausted the entire table. -func (i *singleLevelIterator) loadBlock(dir int8) loadBlockResult { - if !i.index.valid() { - // Ensure the data block iterator is invalidated even if loading of the block - // fails. - i.data.invalidate() - return loadBlockFailed - } - // Load the next block. - v := i.index.value() - bhp, err := decodeBlockHandleWithProperties(v.InPlaceValue()) - if i.dataBH == bhp.BlockHandle && i.data.valid() { - // We're already at the data block we want to load. Reset bounds in case - // they changed since the last seek, but don't reload the block from cache - // or disk. - // - // It's safe to leave i.data in its original state here, as all callers to - // loadBlock make an absolute positioning call (i.e. a seek, first, or last) - // to `i.data` right after loadBlock returns loadBlockOK. - i.initBounds() - return loadBlockOK - } - // Ensure the data block iterator is invalidated even if loading of the block - // fails. - i.data.invalidate() - i.dataBH = bhp.BlockHandle - if err != nil { - i.err = errCorruptIndexEntry - return loadBlockFailed - } - if i.bpfs != nil { - intersects, err := i.bpfs.intersects(bhp.Props) - if err != nil { - i.err = errCorruptIndexEntry - return loadBlockFailed - } - if intersects == blockMaybeExcluded { - intersects = i.resolveMaybeExcluded(dir) - } - if intersects == blockExcluded { - i.maybeFilteredKeysSingleLevel = true - return loadBlockIrrelevant - } - // blockIntersects - } - block, err := i.readBlockWithStats(i.dataBH, i.dataRH) - if err != nil { - i.err = err - return loadBlockFailed - } - i.err = i.data.initHandle(i.cmp, block, i.reader.Properties.GlobalSeqNum) - if i.err != nil { - // The block is partially loaded, and we don't want it to appear valid. - i.data.invalidate() - return loadBlockFailed - } - i.initBounds() - return loadBlockOK -} - -// readBlockForVBR implements the blockProviderWhenOpen interface for use by -// the valueBlockReader. -func (i *singleLevelIterator) readBlockForVBR( - ctx context.Context, h BlockHandle, stats *base.InternalIteratorStats, -) (cache.Handle, error) { - return i.reader.readBlock(ctx, h, nil, i.vbRH, stats) -} - -// resolveMaybeExcluded is invoked when the block-property filterer has found -// that a block is excluded according to its properties but only if its bounds -// fall within the filter's current bounds. This function consults the -// apprioriate bound, depending on the iteration direction, and returns either -// `blockIntersects` or `blockMaybeExcluded`. -func (i *singleLevelIterator) resolveMaybeExcluded(dir int8) intersectsResult { - // TODO(jackson): We could first try comparing to top-level index block's - // key, and if within bounds avoid per-data block key comparisons. - - // This iterator is configured with a bound-limited block property - // filter. The bpf determined this block could be excluded from - // iteration based on the property encoded in the block handle. - // However, we still need to determine if the block is wholly - // contained within the filter's key bounds. - // - // External guarantees ensure all the block's keys are ≥ the - // filter's lower bound during forward iteration, and that all the - // block's keys are < the filter's upper bound during backward - // iteration. We only need to determine if the opposite bound is - // also met. - // - // The index separator in index.Key() provides an inclusive - // upper-bound for the data block's keys, guaranteeing that all its - // keys are ≤ index.Key(). For forward iteration, this is all we - // need. - if dir > 0 { - // Forward iteration. - if i.bpfs.boundLimitedFilter.KeyIsWithinUpperBound(i.index.Key()) { - return blockExcluded - } - return blockIntersects - } - - // Reverse iteration. - // - // Because we're iterating in the reverse direction, we don't yet have - // enough context available to determine if the block is wholly contained - // within its bounds. This case arises only during backward iteration, - // because of the way the index is structured. - // - // Consider a bound-limited bpf limited to the bounds [b,d), loading the - // block with separator `c`. During reverse iteration, the guarantee that - // all the block's keys are < `d` is externally provided, but no guarantee - // is made on the bpf's lower bound. The separator `c` only provides an - // inclusive upper bound on the block's keys, indicating that the - // corresponding block handle points to a block containing only keys ≤ `c`. - // - // To establish a lower bound, we step the index backwards to read the - // previous block's separator, which provides an inclusive lower bound on - // the original block's keys. Afterwards, we step forward to restore our - // index position. - if peekKey, _ := i.index.Prev(); peekKey == nil { - // The original block points to the first block of this index block. If - // there's a two-level index, it could potentially provide a lower - // bound, but the code refactoring necessary to read it doesn't seem - // worth the payoff. We fall through to loading the block. - } else if i.bpfs.boundLimitedFilter.KeyIsWithinLowerBound(peekKey) { - // The lower-bound on the original block falls within the filter's - // bounds, and we can skip the block (after restoring our current index - // position). - _, _ = i.index.Next() - return blockExcluded - } - _, _ = i.index.Next() - return blockIntersects -} - -func (i *singleLevelIterator) readBlockWithStats( - bh BlockHandle, rh objstorage.ReadHandle, -) (cache.Handle, error) { - return i.reader.readBlock(i.ctx, bh, nil, rh, i.stats) -} - -func (i *singleLevelIterator) initBoundsForAlreadyLoadedBlock() { - if i.data.firstKey.UserKey == nil { - panic("initBoundsForAlreadyLoadedBlock must not be called on empty or corrupted block") - } - i.blockLower = i.lower - if i.blockLower != nil { - if i.data.firstKey.UserKey != nil && i.cmp(i.blockLower, i.data.firstKey.UserKey) < 0 { - // The lower-bound is less than the first key in the block. No need - // to check the lower-bound again for this block. - i.blockLower = nil - } - } - i.blockUpper = i.upper - if i.blockUpper != nil && i.cmp(i.blockUpper, i.index.Key().UserKey) > 0 { - // The upper-bound is greater than the index key which itself is greater - // than or equal to every key in the block. No need to check the - // upper-bound again for this block. - i.blockUpper = nil - } -} - -// The number of times to call Next/Prev in a block before giving up and seeking. -// The value of 4 is arbitrary. -// TODO(sumeer): experiment with dynamic adjustment based on the history of -// seeks for a particular iterator. -const numStepsBeforeSeek = 4 - -func (i *singleLevelIterator) trySeekGEUsingNextWithinBlock( - key []byte, -) (k *InternalKey, v base.LazyValue, done bool) { - k, v = i.data.Key(), i.data.value() - for j := 0; j < numStepsBeforeSeek; j++ { - curKeyCmp := i.cmp(k.UserKey, key) - if curKeyCmp >= 0 { - if i.blockUpper != nil && i.cmp(k.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{}, true - } - return k, v, true - } - k, v = i.data.Next() - if k == nil { - break - } - } - return k, v, false -} - -func (i *singleLevelIterator) trySeekLTUsingPrevWithinBlock( - key []byte, -) (k *InternalKey, v base.LazyValue, done bool) { - k, v = i.data.Key(), i.data.value() - for j := 0; j < numStepsBeforeSeek; j++ { - curKeyCmp := i.cmp(k.UserKey, key) - if curKeyCmp < 0 { - if i.blockLower != nil && i.cmp(k.UserKey, i.blockLower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{}, true - } - return k, v, true - } - k, v = i.data.Prev() - if k == nil { - break - } - } - return k, v, false -} - -func (i *singleLevelIterator) recordOffset() uint64 { - offset := i.dataBH.Offset - if i.data.valid() { - // - i.dataBH.Length/len(i.data.data) is the compression ratio. If - // uncompressed, this is 1. - // - i.data.nextOffset is the uncompressed position of the current record - // in the block. - // - i.dataBH.Offset is the offset of the block in the sstable before - // decompression. - offset += (uint64(i.data.nextOffset) * i.dataBH.Length) / uint64(len(i.data.data)) - } else { - // Last entry in the block must increment bytes iterated by the size of the block trailer - // and restart points. - offset += i.dataBH.Length + blockTrailerLen - } - return offset -} - -// SeekGE implements internalIterator.SeekGE, as documented in the pebble -// package. Note that SeekGE only checks the upper bound. It is up to the -// caller to ensure that key is greater than or equal to the lower bound. -func (i *singleLevelIterator) SeekGE( - key []byte, flags base.SeekGEFlags, -) (*InternalKey, base.LazyValue) { - if flags.TrySeekUsingNext() { - // The i.exhaustedBounds comparison indicates that the upper bound was - // reached. The i.data.isDataInvalidated() indicates that the sstable was - // exhausted. - if (i.exhaustedBounds == +1 || i.data.isDataInvalidated()) && i.err == nil { - // Already exhausted, so return nil. - return nil, base.LazyValue{} - } - if i.err != nil { - // The current iterator position cannot be used. - flags = flags.DisableTrySeekUsingNext() - } - // INVARIANT: flags.TrySeekUsingNext() => i.err == nil && - // !i.exhaustedBounds==+1 && !i.data.isDataInvalidated(). That is, - // data-exhausted and bounds-exhausted, as defined earlier, are both - // false. Ths makes it safe to clear out i.exhaustedBounds and i.err - // before calling into seekGEHelper. - } - - i.exhaustedBounds = 0 - i.err = nil // clear cached iteration error - boundsCmp := i.boundsCmp - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - i.positionedUsingLatestBounds = true - return i.seekGEHelper(key, boundsCmp, flags) -} - -// seekGEHelper contains the common functionality for SeekGE and SeekPrefixGE. -func (i *singleLevelIterator) seekGEHelper( - key []byte, boundsCmp int, flags base.SeekGEFlags, -) (*InternalKey, base.LazyValue) { - // Invariant: trySeekUsingNext => !i.data.isDataInvalidated() && i.exhaustedBounds != +1 - - // SeekGE performs various step-instead-of-seeking optimizations: eg enabled - // by trySeekUsingNext, or by monotonically increasing bounds (i.boundsCmp). - // Care must be taken to ensure that when performing these optimizations and - // the iterator becomes exhausted, i.maybeFilteredKeys is set appropriately. - // Consider a previous SeekGE that filtered keys from k until the current - // iterator position. - // - // If the previous SeekGE exhausted the iterator, it's possible keys greater - // than or equal to the current search key were filtered. We must not reuse - // the current iterator position without remembering the previous value of - // maybeFilteredKeys. - - var dontSeekWithinBlock bool - if !i.data.isDataInvalidated() && !i.index.isDataInvalidated() && i.data.valid() && i.index.valid() && - boundsCmp > 0 && i.cmp(key, i.index.Key().UserKey) <= 0 { - // Fast-path: The bounds have moved forward and this SeekGE is - // respecting the lower bound (guaranteed by Iterator). We know that - // the iterator must already be positioned within or just outside the - // previous bounds. Therefore it cannot be positioned at a block (or - // the position within that block) that is ahead of the seek position. - // However it can be positioned at an earlier block. This fast-path to - // use Next() on the block is only applied when we are already at the - // block that the slow-path (the else-clause) would load -- this is - // the motivation for the i.cmp(key, i.index.Key().UserKey) <= 0 - // predicate. - i.initBoundsForAlreadyLoadedBlock() - ikey, val, done := i.trySeekGEUsingNextWithinBlock(key) - if done { - return ikey, val - } - if ikey == nil { - // Done with this block. - dontSeekWithinBlock = true - } - } else { - // Cannot use bounds monotonicity. But may be able to optimize if - // caller claimed externally known invariant represented by - // flags.TrySeekUsingNext(). - if flags.TrySeekUsingNext() { - // seekPrefixGE or SeekGE has already ensured - // !i.data.isDataInvalidated() && i.exhaustedBounds != +1 - currKey := i.data.Key() - value := i.data.value() - less := i.cmp(currKey.UserKey, key) < 0 - // We could be more sophisticated and confirm that the seek - // position is within the current block before applying this - // optimization. But there may be some benefit even if it is in - // the next block, since we can avoid seeking i.index. - for j := 0; less && j < numStepsBeforeSeek; j++ { - currKey, value = i.Next() - if currKey == nil { - return nil, base.LazyValue{} - } - less = i.cmp(currKey.UserKey, key) < 0 - } - if !less { - if i.blockUpper != nil && i.cmp(currKey.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - return currKey, value - } - } - - // Slow-path. - - // Since we're re-seeking the iterator, the previous value of - // maybeFilteredKeysSingleLevel is irrelevant. If we filter out blocks - // during seeking, loadBlock will set it to true. - i.maybeFilteredKeysSingleLevel = false - - var ikey *InternalKey - if ikey, _ = i.index.SeekGE(key, flags.DisableTrySeekUsingNext()); ikey == nil { - // The target key is greater than any key in the index block. - // Invalidate the block iterator so that a subsequent call to Prev() - // will return the last key in the table. - i.data.invalidate() - return nil, base.LazyValue{} - } - result := i.loadBlock(+1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockIrrelevant { - // Enforce the upper bound here since don't want to bother moving - // to the next block if upper bound is already exceeded. Note that - // the next block starts with keys >= ikey.UserKey since even - // though this is the block separator, the same user key can span - // multiple blocks. Since upper is exclusive we use >= below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - // Want to skip to the next block. - dontSeekWithinBlock = true - } - } - if !dontSeekWithinBlock { - if ikey, val := i.data.SeekGE(key, flags.DisableTrySeekUsingNext()); ikey != nil { - if i.blockUpper != nil && i.cmp(ikey.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - return ikey, val - } - } - return i.skipForward() -} - -// SeekPrefixGE implements internalIterator.SeekPrefixGE, as documented in the -// pebble package. Note that SeekPrefixGE only checks the upper bound. It is up -// to the caller to ensure that key is greater than or equal to the lower bound. -func (i *singleLevelIterator) SeekPrefixGE( - prefix, key []byte, flags base.SeekGEFlags, -) (*base.InternalKey, base.LazyValue) { - k, v := i.seekPrefixGE(prefix, key, flags, i.useFilter) - return k, v -} - -func (i *singleLevelIterator) seekPrefixGE( - prefix, key []byte, flags base.SeekGEFlags, checkFilter bool, -) (k *InternalKey, value base.LazyValue) { - // NOTE: prefix is only used for bloom filter checking and not later work in - // this method. Hence, we can use the existing iterator position if the last - // SeekPrefixGE did not fail bloom filter matching. - - err := i.err - i.err = nil // clear cached iteration error - if checkFilter && i.reader.tableFilter != nil { - if !i.lastBloomFilterMatched { - // Iterator is not positioned based on last seek. - flags = flags.DisableTrySeekUsingNext() - } - i.lastBloomFilterMatched = false - // Check prefix bloom filter. - var dataH cache.Handle - dataH, i.err = i.reader.readFilter(i.ctx, i.stats) - if i.err != nil { - i.data.invalidate() - return nil, base.LazyValue{} - } - mayContain := i.reader.tableFilter.mayContain(dataH.Get(), prefix) - dataH.Release() - if !mayContain { - // This invalidation may not be necessary for correctness, and may - // be a place to optimize later by reusing the already loaded - // block. It was necessary in earlier versions of the code since - // the caller was allowed to call Next when SeekPrefixGE returned - // nil. This is no longer allowed. - i.data.invalidate() - return nil, base.LazyValue{} - } - i.lastBloomFilterMatched = true - } - if flags.TrySeekUsingNext() { - // The i.exhaustedBounds comparison indicates that the upper bound was - // reached. The i.data.isDataInvalidated() indicates that the sstable was - // exhausted. - if (i.exhaustedBounds == +1 || i.data.isDataInvalidated()) && err == nil { - // Already exhausted, so return nil. - return nil, base.LazyValue{} - } - if err != nil { - // The current iterator position cannot be used. - flags = flags.DisableTrySeekUsingNext() - } - // INVARIANT: flags.TrySeekUsingNext() => err == nil && - // !i.exhaustedBounds==+1 && !i.data.isDataInvalidated(). That is, - // data-exhausted and bounds-exhausted, as defined earlier, are both - // false. Ths makes it safe to clear out i.exhaustedBounds and i.err - // before calling into seekGEHelper. - } - // Bloom filter matches, or skipped, so this method will position the - // iterator. - i.exhaustedBounds = 0 - boundsCmp := i.boundsCmp - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - i.positionedUsingLatestBounds = true - k, value = i.seekGEHelper(key, boundsCmp, flags) - return k, value -} - -// SeekLT implements internalIterator.SeekLT, as documented in the pebble -// package. Note that SeekLT only checks the lower bound. It is up to the -// caller to ensure that key is less than the upper bound. -func (i *singleLevelIterator) SeekLT( - key []byte, flags base.SeekLTFlags, -) (*InternalKey, base.LazyValue) { - i.exhaustedBounds = 0 - i.err = nil // clear cached iteration error - boundsCmp := i.boundsCmp - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - // Seeking operations perform various step-instead-of-seeking optimizations: - // eg by considering monotonically increasing bounds (i.boundsCmp). Care - // must be taken to ensure that when performing these optimizations and the - // iterator becomes exhausted i.maybeFilteredKeysSingleLevel is set - // appropriately. Consider a previous SeekLT that filtered keys from k - // until the current iterator position. - // - // If the previous SeekLT did exhausted the iterator, it's possible keys - // less than the current search key were filtered. We must not reuse the - // current iterator position without remembering the previous value of - // maybeFilteredKeysSingleLevel. - - i.positionedUsingLatestBounds = true - - var dontSeekWithinBlock bool - if !i.data.isDataInvalidated() && !i.index.isDataInvalidated() && i.data.valid() && i.index.valid() && - boundsCmp < 0 && i.cmp(i.data.firstKey.UserKey, key) < 0 { - // Fast-path: The bounds have moved backward, and this SeekLT is - // respecting the upper bound (guaranteed by Iterator). We know that - // the iterator must already be positioned within or just outside the - // previous bounds. Therefore it cannot be positioned at a block (or - // the position within that block) that is behind the seek position. - // However it can be positioned at a later block. This fast-path to - // use Prev() on the block is only applied when we are already at the - // block that can satisfy this seek -- this is the motivation for the - // the i.cmp(i.data.firstKey.UserKey, key) < 0 predicate. - i.initBoundsForAlreadyLoadedBlock() - ikey, val, done := i.trySeekLTUsingPrevWithinBlock(key) - if done { - return ikey, val - } - if ikey == nil { - // Done with this block. - dontSeekWithinBlock = true - } - } else { - // Slow-path. - i.maybeFilteredKeysSingleLevel = false - var ikey *InternalKey - - // NB: If a bound-limited block property filter is configured, it's - // externally ensured that the filter is disabled (through returning - // Intersects=false irrespective of the block props provided) during - // seeks. - if ikey, _ = i.index.SeekGE(key, base.SeekGEFlagsNone); ikey == nil { - ikey, _ = i.index.Last() - if ikey == nil { - return nil, base.LazyValue{} - } - } - // INVARIANT: ikey != nil. - result := i.loadBlock(-1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockIrrelevant { - // Enforce the lower bound here since don't want to bother moving - // to the previous block if lower bound is already exceeded. Note - // that the previous block starts with keys <= ikey.UserKey since - // even though this is the current block's separator, the same - // user key can span multiple blocks. - if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{} - } - // Want to skip to the previous block. - dontSeekWithinBlock = true - } - } - if !dontSeekWithinBlock { - if ikey, val := i.data.SeekLT(key, flags); ikey != nil { - if i.blockLower != nil && i.cmp(ikey.UserKey, i.blockLower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{} - } - return ikey, val - } - } - // The index contains separator keys which may lie between - // user-keys. Consider the user-keys: - // - // complete - // ---- new block --- - // complexion - // - // If these two keys end one block and start the next, the index key may - // be chosen as "compleu". The SeekGE in the index block will then point - // us to the block containing "complexion". If this happens, we want the - // last key from the previous data block. - return i.skipBackward() -} - -// First implements internalIterator.First, as documented in the pebble -// package. Note that First only checks the upper bound. It is up to the caller -// to ensure that key is greater than or equal to the lower bound (e.g. via a -// call to SeekGE(lower)). -func (i *singleLevelIterator) First() (*InternalKey, base.LazyValue) { - if i.lower != nil { - panic("singleLevelIterator.First() used despite lower bound") - } - i.positionedUsingLatestBounds = true - i.maybeFilteredKeysSingleLevel = false - return i.firstInternal() -} - -// firstInternal is a helper used for absolute positioning in a single-level -// index file, or for positioning in the second-level index in a two-level -// index file. For the latter, one cannot make any claims about absolute -// positioning. -func (i *singleLevelIterator) firstInternal() (*InternalKey, base.LazyValue) { - i.exhaustedBounds = 0 - i.err = nil // clear cached iteration error - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - var ikey *InternalKey - if ikey, _ = i.index.First(); ikey == nil { - i.data.invalidate() - return nil, base.LazyValue{} - } - result := i.loadBlock(+1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.data.First(); ikey != nil { - if i.blockUpper != nil && i.cmp(ikey.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - return ikey, val - } - // Else fall through to skipForward. - } else { - // result == loadBlockIrrelevant. Enforce the upper bound here since - // don't want to bother moving to the next block if upper bound is - // already exceeded. Note that the next block starts with keys >= - // ikey.UserKey since even though this is the block separator, the - // same user key can span multiple blocks. Since upper is exclusive we - // use >= below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - // Else fall through to skipForward. - } - - return i.skipForward() -} - -// Last implements internalIterator.Last, as documented in the pebble -// package. Note that Last only checks the lower bound. It is up to the caller -// to ensure that key is less than the upper bound (e.g. via a call to -// SeekLT(upper)) -func (i *singleLevelIterator) Last() (*InternalKey, base.LazyValue) { - if i.upper != nil { - panic("singleLevelIterator.Last() used despite upper bound") - } - i.positionedUsingLatestBounds = true - i.maybeFilteredKeysSingleLevel = false - return i.lastInternal() -} - -// lastInternal is a helper used for absolute positioning in a single-level -// index file, or for positioning in the second-level index in a two-level -// index file. For the latter, one cannot make any claims about absolute -// positioning. -func (i *singleLevelIterator) lastInternal() (*InternalKey, base.LazyValue) { - i.exhaustedBounds = 0 - i.err = nil // clear cached iteration error - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - var ikey *InternalKey - if ikey, _ = i.index.Last(); ikey == nil { - i.data.invalidate() - return nil, base.LazyValue{} - } - result := i.loadBlock(-1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.data.Last(); ikey != nil { - if i.blockLower != nil && i.cmp(ikey.UserKey, i.blockLower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{} - } - return ikey, val - } - // Else fall through to skipBackward. - } else { - // result == loadBlockIrrelevant. Enforce the lower bound here since - // don't want to bother moving to the previous block if lower bound is - // already exceeded. Note that the previous block starts with keys <= - // key.UserKey since even though this is the current block's - // separator, the same user key can span multiple blocks. - if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{} - } - } - - return i.skipBackward() -} - -// Next implements internalIterator.Next, as documented in the pebble -// package. -// Note: compactionIterator.Next mirrors the implementation of Iterator.Next -// due to performance. Keep the two in sync. -func (i *singleLevelIterator) Next() (*InternalKey, base.LazyValue) { - if i.exhaustedBounds == +1 { - panic("Next called even though exhausted upper bound") - } - i.exhaustedBounds = 0 - i.maybeFilteredKeysSingleLevel = false - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - if i.err != nil { - return nil, base.LazyValue{} - } - if key, val := i.data.Next(); key != nil { - if i.blockUpper != nil && i.cmp(key.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - return key, val - } - return i.skipForward() -} - -// NextPrefix implements (base.InternalIterator).NextPrefix. -func (i *singleLevelIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { - if i.exhaustedBounds == +1 { - panic("NextPrefix called even though exhausted upper bound") - } - i.exhaustedBounds = 0 - i.maybeFilteredKeysSingleLevel = false - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - if i.err != nil { - return nil, base.LazyValue{} - } - if key, val := i.data.nextPrefix(succKey); key != nil { - if i.blockUpper != nil && i.cmp(key.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - return key, val - } - // Did not find prefix in the existing data block. This is the slow-path - // where we effectively seek the iterator. - var ikey *InternalKey - // The key is likely to be in the next data block, so try one step. - if ikey, _ = i.index.Next(); ikey == nil { - // The target key is greater than any key in the index block. - // Invalidate the block iterator so that a subsequent call to Prev() - // will return the last key in the table. - i.data.invalidate() - return nil, base.LazyValue{} - } - if i.cmp(succKey, ikey.UserKey) > 0 { - // Not in the next data block, so seek the index. - if ikey, _ = i.index.SeekGE(succKey, base.SeekGEFlagsNone); ikey == nil { - // The target key is greater than any key in the index block. - // Invalidate the block iterator so that a subsequent call to Prev() - // will return the last key in the table. - i.data.invalidate() - return nil, base.LazyValue{} - } - } - result := i.loadBlock(+1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockIrrelevant { - // Enforce the upper bound here since don't want to bother moving - // to the next block if upper bound is already exceeded. Note that - // the next block starts with keys >= ikey.UserKey since even - // though this is the block separator, the same user key can span - // multiple blocks. Since upper is exclusive we use >= below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - } else if key, val := i.data.SeekGE(succKey, base.SeekGEFlagsNone); key != nil { - if i.blockUpper != nil && i.cmp(key.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - return key, val - } - return i.skipForward() -} - -// Prev implements internalIterator.Prev, as documented in the pebble -// package. -func (i *singleLevelIterator) Prev() (*InternalKey, base.LazyValue) { - if i.exhaustedBounds == -1 { - panic("Prev called even though exhausted lower bound") - } - i.exhaustedBounds = 0 - i.maybeFilteredKeysSingleLevel = false - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - if i.err != nil { - return nil, base.LazyValue{} - } - if key, val := i.data.Prev(); key != nil { - if i.blockLower != nil && i.cmp(key.UserKey, i.blockLower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{} - } - return key, val - } - return i.skipBackward() -} - -func (i *singleLevelIterator) skipForward() (*InternalKey, base.LazyValue) { - for { - var key *InternalKey - if key, _ = i.index.Next(); key == nil { - i.data.invalidate() - break - } - result := i.loadBlock(+1) - if result != loadBlockOK { - if i.err != nil { - break - } - if result == loadBlockFailed { - // We checked that i.index was at a valid entry, so - // loadBlockFailed could not have happened due to to i.index - // being exhausted, and must be due to an error. - panic("loadBlock should not have failed with no error") - } - // result == loadBlockIrrelevant. Enforce the upper bound here - // since don't want to bother moving to the next block if upper - // bound is already exceeded. Note that the next block starts with - // keys >= key.UserKey since even though this is the block - // separator, the same user key can span multiple blocks. Since - // upper is exclusive we use >= below. - if i.upper != nil && i.cmp(key.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - continue - } - if key, val := i.data.First(); key != nil { - if i.blockUpper != nil && i.cmp(key.UserKey, i.blockUpper) >= 0 { - i.exhaustedBounds = +1 - return nil, base.LazyValue{} - } - return key, val - } - } - return nil, base.LazyValue{} -} - -func (i *singleLevelIterator) skipBackward() (*InternalKey, base.LazyValue) { - for { - var key *InternalKey - if key, _ = i.index.Prev(); key == nil { - i.data.invalidate() - break - } - result := i.loadBlock(-1) - if result != loadBlockOK { - if i.err != nil { - break - } - if result == loadBlockFailed { - // We checked that i.index was at a valid entry, so - // loadBlockFailed could not have happened due to to i.index - // being exhausted, and must be due to an error. - panic("loadBlock should not have failed with no error") - } - // result == loadBlockIrrelevant. Enforce the lower bound here - // since don't want to bother moving to the previous block if lower - // bound is already exceeded. Note that the previous block starts with - // keys <= key.UserKey since even though this is the current block's - // separator, the same user key can span multiple blocks. - if i.lower != nil && i.cmp(key.UserKey, i.lower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{} - } - continue - } - key, val := i.data.Last() - if key == nil { - return nil, base.LazyValue{} - } - if i.blockLower != nil && i.cmp(key.UserKey, i.blockLower) < 0 { - i.exhaustedBounds = -1 - return nil, base.LazyValue{} - } - return key, val - } - return nil, base.LazyValue{} -} - -// Error implements internalIterator.Error, as documented in the pebble -// package. -func (i *singleLevelIterator) Error() error { - if err := i.data.Error(); err != nil { - return err - } - return i.err -} - -// MaybeFilteredKeys may be called when an iterator is exhausted to indicate -// whether or not the last positioning method may have skipped any keys due to -// block-property filters. -func (i *singleLevelIterator) MaybeFilteredKeys() bool { - return i.maybeFilteredKeysSingleLevel -} - -// SetCloseHook sets a function that will be called when the iterator is -// closed. -func (i *singleLevelIterator) SetCloseHook(fn func(i Iterator) error) { - i.closeHook = fn -} - -func firstError(err0, err1 error) error { - if err0 != nil { - return err0 - } - return err1 -} - -// Close implements internalIterator.Close, as documented in the pebble -// package. -func (i *singleLevelIterator) Close() error { - var err error - if i.closeHook != nil { - err = firstError(err, i.closeHook(i)) - } - err = firstError(err, i.data.Close()) - err = firstError(err, i.index.Close()) - if i.dataRH != nil { - err = firstError(err, i.dataRH.Close()) - i.dataRH = nil - } - err = firstError(err, i.err) - if i.bpfs != nil { - releaseBlockPropertiesFilterer(i.bpfs) - } - if i.vbReader != nil { - i.vbReader.close() - } - if i.vbRH != nil { - err = firstError(err, i.vbRH.Close()) - i.vbRH = nil - } - *i = i.resetForReuse() - singleLevelIterPool.Put(i) - return err -} - -func (i *singleLevelIterator) String() string { - return i.reader.fileNum.String() -} - -// Deterministic disabling of the bounds-based optimization that avoids seeking. -// Uses the iterator pointer, since we want diversity in iterator behavior for -// the same SetBounds call. Used for tests. -func disableBoundsOpt(bound []byte, ptr uintptr) bool { - // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ - simpleHash := (11400714819323198485 * uint64(ptr)) >> 63 - return bound[len(bound)-1]&byte(1) == 0 && simpleHash == 0 -} - -// ensureBoundsOptDeterminism provides a facility for disabling of the bounds -// optimizations performed by disableBoundsOpt for tests that require -// deterministic iterator behavior. Some unit tests examine internal iterator -// state and require this behavior to be deterministic. -var ensureBoundsOptDeterminism bool - -// SetBounds implements internalIterator.SetBounds, as documented in the pebble -// package. -func (i *singleLevelIterator) SetBounds(lower, upper []byte) { - i.boundsCmp = 0 - if i.positionedUsingLatestBounds { - if i.upper != nil && lower != nil && i.cmp(i.upper, lower) <= 0 { - i.boundsCmp = +1 - if invariants.Enabled && !ensureBoundsOptDeterminism && - disableBoundsOpt(lower, uintptr(unsafe.Pointer(i))) { - i.boundsCmp = 0 - } - } else if i.lower != nil && upper != nil && i.cmp(upper, i.lower) <= 0 { - i.boundsCmp = -1 - if invariants.Enabled && !ensureBoundsOptDeterminism && - disableBoundsOpt(upper, uintptr(unsafe.Pointer(i))) { - i.boundsCmp = 0 - } - } - i.positionedUsingLatestBounds = false - } - i.lower = lower - i.upper = upper - i.blockLower = nil - i.blockUpper = nil -} - -var _ base.InternalIterator = &singleLevelIterator{} -var _ base.InternalIterator = &twoLevelIterator{} - -// compactionIterator is similar to Iterator but it increments the number of -// bytes that have been iterated through. -type compactionIterator struct { - *singleLevelIterator - bytesIterated *uint64 - prevOffset uint64 -} - -// compactionIterator implements the base.InternalIterator interface. -var _ base.InternalIterator = (*compactionIterator)(nil) - -func (i *compactionIterator) String() string { - return i.reader.fileNum.String() -} - -func (i *compactionIterator) SeekGE( - key []byte, flags base.SeekGEFlags, -) (*InternalKey, base.LazyValue) { - panic("pebble: SeekGE unimplemented") -} - -func (i *compactionIterator) SeekPrefixGE( - prefix, key []byte, flags base.SeekGEFlags, -) (*base.InternalKey, base.LazyValue) { - panic("pebble: SeekPrefixGE unimplemented") -} - -func (i *compactionIterator) SeekLT( - key []byte, flags base.SeekLTFlags, -) (*InternalKey, base.LazyValue) { - panic("pebble: SeekLT unimplemented") -} - -func (i *compactionIterator) First() (*InternalKey, base.LazyValue) { - i.err = nil // clear cached iteration error - return i.skipForward(i.singleLevelIterator.First()) -} - -func (i *compactionIterator) Last() (*InternalKey, base.LazyValue) { - panic("pebble: Last unimplemented") -} - -// Note: compactionIterator.Next mirrors the implementation of Iterator.Next -// due to performance. Keep the two in sync. -func (i *compactionIterator) Next() (*InternalKey, base.LazyValue) { - if i.err != nil { - return nil, base.LazyValue{} - } - return i.skipForward(i.data.Next()) -} - -func (i *compactionIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { - panic("pebble: NextPrefix unimplemented") -} - -func (i *compactionIterator) Prev() (*InternalKey, base.LazyValue) { - panic("pebble: Prev unimplemented") -} - -func (i *compactionIterator) skipForward( - key *InternalKey, val base.LazyValue, -) (*InternalKey, base.LazyValue) { - if key == nil { - for { - if key, _ := i.index.Next(); key == nil { - break - } - result := i.loadBlock(+1) - if result != loadBlockOK { - if i.err != nil { - break - } - switch result { - case loadBlockFailed: - // We checked that i.index was at a valid entry, so - // loadBlockFailed could not have happened due to to i.index - // being exhausted, and must be due to an error. - panic("loadBlock should not have failed with no error") - case loadBlockIrrelevant: - panic("compactionIter should not be using block intervals for skipping") - default: - panic(fmt.Sprintf("unexpected case %d", result)) - } - } - // result == loadBlockOK - if key, val = i.data.First(); key != nil { - break - } - } - } - - curOffset := i.recordOffset() - *i.bytesIterated += uint64(curOffset - i.prevOffset) - i.prevOffset = curOffset - return key, val -} - -type twoLevelIterator struct { - singleLevelIterator - // maybeFilteredKeysSingleLevel indicates whether the last iterator - // positioning operation may have skipped any index blocks due to - // block-property filters when positioning the top-level-index. - maybeFilteredKeysTwoLevel bool - topLevelIndex blockIter -} - -// twoLevelIterator implements the base.InternalIterator interface. -var _ base.InternalIterator = (*twoLevelIterator)(nil) - -// loadIndex loads the index block at the current top level index position and -// leaves i.index unpositioned. If unsuccessful, it gets i.err to any error -// encountered, which may be nil if we have simply exhausted the entire table. -// This is used for two level indexes. -func (i *twoLevelIterator) loadIndex(dir int8) loadBlockResult { - // Ensure the index data block iterators are invalidated even if loading of - // the index fails. - i.data.invalidate() - i.index.invalidate() - if !i.topLevelIndex.valid() { - i.index.offset = 0 - i.index.restarts = 0 - return loadBlockFailed - } - v := i.topLevelIndex.value() - bhp, err := decodeBlockHandleWithProperties(v.InPlaceValue()) - if err != nil { - i.err = base.CorruptionErrorf("pebble/table: corrupt top level index entry") - return loadBlockFailed - } - if i.bpfs != nil { - intersects, err := i.bpfs.intersects(bhp.Props) - if err != nil { - i.err = errCorruptIndexEntry - return loadBlockFailed - } - if intersects == blockMaybeExcluded { - intersects = i.resolveMaybeExcluded(dir) - } - if intersects == blockExcluded { - i.maybeFilteredKeysTwoLevel = true - return loadBlockIrrelevant - } - // blockIntersects - } - indexBlock, err := i.readBlockWithStats(bhp.BlockHandle, nil /* readHandle */) - if err != nil { - i.err = err - return loadBlockFailed - } - if i.err = i.index.initHandle( - i.cmp, indexBlock, i.reader.Properties.GlobalSeqNum); i.err == nil { - return loadBlockOK - } - return loadBlockFailed -} - -// resolveMaybeExcluded is invoked when the block-property filterer has found -// that an index block is excluded according to its properties but only if its -// bounds fall within the filter's current bounds. This function consults the -// apprioriate bound, depending on the iteration direction, and returns either -// `blockIntersects` or -// `blockMaybeExcluded`. -func (i *twoLevelIterator) resolveMaybeExcluded(dir int8) intersectsResult { - // This iterator is configured with a bound-limited block property filter. - // The bpf determined this entire index block could be excluded from - // iteration based on the property encoded in the block handle. However, we - // still need to determine if the index block is wholly contained within the - // filter's key bounds. - // - // External guarantees ensure all its data blocks' keys are ≥ the filter's - // lower bound during forward iteration, and that all its data blocks' keys - // are < the filter's upper bound during backward iteration. We only need to - // determine if the opposite bound is also met. - // - // The index separator in topLevelIndex.Key() provides an inclusive - // upper-bound for the index block's keys, guaranteeing that all its keys - // are ≤ topLevelIndex.Key(). For forward iteration, this is all we need. - if dir > 0 { - // Forward iteration. - if i.bpfs.boundLimitedFilter.KeyIsWithinUpperBound(i.topLevelIndex.Key()) { - return blockExcluded - } - return blockIntersects - } - - // Reverse iteration. - // - // Because we're iterating in the reverse direction, we don't yet have - // enough context available to determine if the block is wholly contained - // within its bounds. This case arises only during backward iteration, - // because of the way the index is structured. - // - // Consider a bound-limited bpf limited to the bounds [b,d), loading the - // block with separator `c`. During reverse iteration, the guarantee that - // all the block's keys are < `d` is externally provided, but no guarantee - // is made on the bpf's lower bound. The separator `c` only provides an - // inclusive upper bound on the block's keys, indicating that the - // corresponding block handle points to a block containing only keys ≤ `c`. - // - // To establish a lower bound, we step the top-level index backwards to read - // the previous block's separator, which provides an inclusive lower bound - // on the original index block's keys. Afterwards, we step forward to - // restore our top-level index position. - if peekKey, _ := i.topLevelIndex.Prev(); peekKey == nil { - // The original block points to the first index block of this table. If - // we knew the lower bound for the entire table, it could provide a - // lower bound, but the code refactoring necessary to read it doesn't - // seem worth the payoff. We fall through to loading the block. - } else if i.bpfs.boundLimitedFilter.KeyIsWithinLowerBound(peekKey) { - // The lower-bound on the original index block falls within the filter's - // bounds, and we can skip the block (after restoring our current - // top-level index position). - _, _ = i.topLevelIndex.Next() - return blockExcluded - } - _, _ = i.topLevelIndex.Next() - return blockIntersects -} - -func (i *twoLevelIterator) init( - ctx context.Context, - r *Reader, - lower, upper []byte, - filterer *BlockPropertiesFilterer, - useFilter bool, - stats *base.InternalIteratorStats, - rp ReaderProvider, -) error { - if r.err != nil { - return r.err - } - topLevelIndexH, err := r.readIndex(ctx, stats) - if err != nil { - return err - } - - i.ctx = ctx - i.lower = lower - i.upper = upper - i.bpfs = filterer - i.useFilter = useFilter - i.reader = r - i.cmp = r.Compare - i.stats = stats - err = i.topLevelIndex.initHandle(i.cmp, topLevelIndexH, r.Properties.GlobalSeqNum) - if err != nil { - // blockIter.Close releases topLevelIndexH and always returns a nil error - _ = i.topLevelIndex.Close() - return err - } - i.dataRH = r.readable.NewReadHandle() - if r.tableFormat == TableFormatPebblev3 { - if r.Properties.NumValueBlocks > 0 { - i.vbReader = &valueBlockReader{ - ctx: ctx, - bpOpen: i, - rp: rp, - vbih: r.valueBIH, - stats: stats, - } - i.data.lazyValueHandling.vbr = i.vbReader - i.vbRH = r.readable.NewReadHandle() - } - i.data.lazyValueHandling.hasValuePrefix = true - } - return nil -} - -func (i *twoLevelIterator) String() string { - return i.reader.fileNum.String() -} - -// MaybeFilteredKeys may be called when an iterator is exhausted to indicate -// whether or not the last positioning method may have skipped any keys due to -// block-property filters. -func (i *twoLevelIterator) MaybeFilteredKeys() bool { - // While reading sstables with two-level indexes, knowledge of whether we've - // filtered keys is tracked separately for each index level. The - // seek-using-next optimizations have different criteria. We can only reset - // maybeFilteredKeys back to false during a seek when NOT using the - // fast-path that uses the current iterator position. - // - // If either level might have filtered keys to arrive at the current - // iterator position, return MaybeFilteredKeys=true. - return i.maybeFilteredKeysTwoLevel || i.maybeFilteredKeysSingleLevel -} - -// SeekGE implements internalIterator.SeekGE, as documented in the pebble -// package. Note that SeekGE only checks the upper bound. It is up to the -// caller to ensure that key is greater than or equal to the lower bound. -func (i *twoLevelIterator) SeekGE( - key []byte, flags base.SeekGEFlags, -) (*InternalKey, base.LazyValue) { - err := i.err - i.err = nil // clear cached iteration error - - // The twoLevelIterator could be already exhausted. Utilize that when - // trySeekUsingNext is true. See the comment about data-exhausted, PGDE, and - // bounds-exhausted near the top of the file. - if flags.TrySeekUsingNext() && - (i.exhaustedBounds == +1 || (i.data.isDataInvalidated() && i.index.isDataInvalidated())) && - err == nil { - // Already exhausted, so return nil. - return nil, base.LazyValue{} - } - - // SeekGE performs various step-instead-of-seeking optimizations: eg enabled - // by trySeekUsingNext, or by monotonically increasing bounds (i.boundsCmp). - // Care must be taken to ensure that when performing these optimizations and - // the iterator becomes exhausted, i.maybeFilteredKeys is set appropriately. - // Consider a previous SeekGE that filtered keys from k until the current - // iterator position. - // - // If the previous SeekGE exhausted the iterator while seeking within the - // two-level index, it's possible keys greater than or equal to the current - // search key were filtered through skipped index blocks. We must not reuse - // the position of the two-level index iterator without remembering the - // previous value of maybeFilteredKeys. - - // We fall into the slow path if i.index.isDataInvalidated() even if the - // top-level iterator is already positioned correctly and all other - // conditions are met. An alternative structure could reuse topLevelIndex's - // current position and reload the index block to which it points. Arguably, - // an index block load is expensive and the index block may still be earlier - // than the index block containing the sought key, resulting in a wasteful - // block load. - - var dontSeekWithinSingleLevelIter bool - if i.topLevelIndex.isDataInvalidated() || !i.topLevelIndex.valid() || i.index.isDataInvalidated() || err != nil || - (i.boundsCmp <= 0 && !flags.TrySeekUsingNext()) || i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 { - // Slow-path: need to position the topLevelIndex. - - // The previous exhausted state of singleLevelIterator is no longer - // relevant, since we may be moving to a different index block. - i.exhaustedBounds = 0 - i.maybeFilteredKeysTwoLevel = false - flags = flags.DisableTrySeekUsingNext() - var ikey *InternalKey - if ikey, _ = i.topLevelIndex.SeekGE(key, flags); ikey == nil { - i.data.invalidate() - i.index.invalidate() - return nil, base.LazyValue{} - } - - result := i.loadIndex(+1) - if result == loadBlockFailed { - i.boundsCmp = 0 - return nil, base.LazyValue{} - } - if result == loadBlockIrrelevant { - // Enforce the upper bound here since don't want to bother moving - // to the next entry in the top level index if upper bound is - // already exceeded. Note that the next entry starts with keys >= - // ikey.UserKey since even though this is the block separator, the - // same user key can span multiple index blocks. Since upper is - // exclusive we use >= below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - } - // Fall through to skipForward. - dontSeekWithinSingleLevelIter = true - // Clear boundsCmp. - // - // In the typical cases where dontSeekWithinSingleLevelIter=false, - // the singleLevelIterator.SeekGE call will clear boundsCmp. - // However, in this case where dontSeekWithinSingleLevelIter=true, - // we never seek on the single-level iterator. This call will fall - // through to skipForward, which may improperly leave boundsCmp=+1 - // unless we clear it here. - i.boundsCmp = 0 - } - } else { - // INVARIANT: err == nil. - // - // Else fast-path: There are two possible cases, from - // (i.boundsCmp > 0 || flags.TrySeekUsingNext()): - // - // 1) The bounds have moved forward (i.boundsCmp > 0) and this SeekGE is - // respecting the lower bound (guaranteed by Iterator). We know that the - // iterator must already be positioned within or just outside the previous - // bounds. Therefore, the topLevelIndex iter cannot be positioned at an - // entry ahead of the seek position (though it can be positioned behind). - // The !i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 confirms that it is - // not behind. Since it is not ahead and not behind it must be at the - // right position. - // - // 2) This SeekGE will land on a key that is greater than the key we are - // currently at (guaranteed by trySeekUsingNext), but since i.cmp(key, - // i.topLevelIndex.Key().UserKey) <= 0, we are at the correct lower level - // index block. No need to reset the state of singleLevelIterator. - // - // Note that cases 1 and 2 never overlap, and one of them must be true, - // but we have some test code (TestIterRandomizedMaybeFilteredKeys) that - // sets both to true, so we fix things here and then do an invariant - // check. - // - // This invariant checking is important enough that we do not gate it - // behind invariants.Enabled. - if i.boundsCmp > 0 { - // TODO(sumeer): fix TestIterRandomizedMaybeFilteredKeys so as to not - // need this behavior. - flags = flags.DisableTrySeekUsingNext() - } - if i.boundsCmp > 0 == flags.TrySeekUsingNext() { - panic(fmt.Sprintf("inconsistency in optimization case 1 %t and case 2 %t", - i.boundsCmp > 0, flags.TrySeekUsingNext())) - } - - if !flags.TrySeekUsingNext() { - // Case 1. Bounds have changed so the previous exhausted bounds state is - // irrelevant. - // WARNING-data-exhausted: this is safe to do only because the monotonic - // bounds optimizations only work when !data-exhausted. If they also - // worked with data-exhausted, we have made it unclear whether - // data-exhausted is actually true. See the comment at the top of the - // file. - i.exhaustedBounds = 0 - } - // Else flags.TrySeekUsingNext(). The i.exhaustedBounds is important to - // preserve for singleLevelIterator, and twoLevelIterator.skipForward. See - // bug https://github.com/cockroachdb/pebble/issues/2036. - } - - if !dontSeekWithinSingleLevelIter { - // Note that while trySeekUsingNext could be false here, singleLevelIterator - // could do its own boundsCmp-based optimization to seek using next. - if ikey, val := i.singleLevelIterator.SeekGE(key, flags); ikey != nil { - return ikey, val - } - } - return i.skipForward() -} - -// SeekPrefixGE implements internalIterator.SeekPrefixGE, as documented in the -// pebble package. Note that SeekPrefixGE only checks the upper bound. It is up -// to the caller to ensure that key is greater than or equal to the lower bound. -func (i *twoLevelIterator) SeekPrefixGE( - prefix, key []byte, flags base.SeekGEFlags, -) (*base.InternalKey, base.LazyValue) { - // NOTE: prefix is only used for bloom filter checking and not later work in - // this method. Hence, we can use the existing iterator position if the last - // SeekPrefixGE did not fail bloom filter matching. - - err := i.err - i.err = nil // clear cached iteration error - - // The twoLevelIterator could be already exhausted. Utilize that when - // trySeekUsingNext is true. See the comment about data-exhausted, PGDE, and - // bounds-exhausted near the top of the file. - filterUsedAndDidNotMatch := - i.reader.tableFilter != nil && i.useFilter && !i.lastBloomFilterMatched - if flags.TrySeekUsingNext() && !filterUsedAndDidNotMatch && - (i.exhaustedBounds == +1 || (i.data.isDataInvalidated() && i.index.isDataInvalidated())) && - err == nil { - // Already exhausted, so return nil. - return nil, base.LazyValue{} - } - - // Check prefix bloom filter. - if i.reader.tableFilter != nil && i.useFilter { - if !i.lastBloomFilterMatched { - // Iterator is not positioned based on last seek. - flags = flags.DisableTrySeekUsingNext() - } - i.lastBloomFilterMatched = false - var dataH cache.Handle - dataH, i.err = i.reader.readFilter(i.ctx, i.stats) - if i.err != nil { - i.data.invalidate() - return nil, base.LazyValue{} - } - mayContain := i.reader.tableFilter.mayContain(dataH.Get(), prefix) - dataH.Release() - if !mayContain { - // This invalidation may not be necessary for correctness, and may - // be a place to optimize later by reusing the already loaded - // block. It was necessary in earlier versions of the code since - // the caller was allowed to call Next when SeekPrefixGE returned - // nil. This is no longer allowed. - i.data.invalidate() - return nil, base.LazyValue{} - } - i.lastBloomFilterMatched = true - } - - // Bloom filter matches. - - // SeekPrefixGE performs various step-instead-of-seeking optimizations: eg - // enabled by trySeekUsingNext, or by monotonically increasing bounds - // (i.boundsCmp). Care must be taken to ensure that when performing these - // optimizations and the iterator becomes exhausted, - // i.maybeFilteredKeysTwoLevel is set appropriately. Consider a previous - // SeekPrefixGE that filtered keys from k until the current iterator - // position. - // - // If the previous SeekPrefixGE exhausted the iterator while seeking within - // the two-level index, it's possible keys greater than or equal to the - // current search key were filtered through skipped index blocks. We must - // not reuse the position of the two-level index iterator without - // remembering the previous value of maybeFilteredKeysTwoLevel. - - // We fall into the slow path if i.index.isDataInvalidated() even if the - // top-level iterator is already positioned correctly and all other - // conditions are met. An alternative structure could reuse topLevelIndex's - // current position and reload the index block to which it points. Arguably, - // an index block load is expensive and the index block may still be earlier - // than the index block containing the sought key, resulting in a wasteful - // block load. - - var dontSeekWithinSingleLevelIter bool - if i.topLevelIndex.isDataInvalidated() || !i.topLevelIndex.valid() || i.index.isDataInvalidated() || err != nil || - (i.boundsCmp <= 0 && !flags.TrySeekUsingNext()) || i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 { - // Slow-path: need to position the topLevelIndex. - - // The previous exhausted state of singleLevelIterator is no longer - // relevant, since we may be moving to a different index block. - i.exhaustedBounds = 0 - i.maybeFilteredKeysTwoLevel = false - flags = flags.DisableTrySeekUsingNext() - var ikey *InternalKey - if ikey, _ = i.topLevelIndex.SeekGE(key, flags); ikey == nil { - i.data.invalidate() - i.index.invalidate() - return nil, base.LazyValue{} - } - - result := i.loadIndex(+1) - if result == loadBlockFailed { - i.boundsCmp = 0 - return nil, base.LazyValue{} - } - if result == loadBlockIrrelevant { - // Enforce the upper bound here since don't want to bother moving - // to the next entry in the top level index if upper bound is - // already exceeded. Note that the next entry starts with keys >= - // ikey.UserKey since even though this is the block separator, the - // same user key can span multiple index blocks. Since upper is - // exclusive we use >= below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - } - // Fall through to skipForward. - dontSeekWithinSingleLevelIter = true - // Clear boundsCmp. - // - // In the typical cases where dontSeekWithinSingleLevelIter=false, - // the singleLevelIterator.SeekPrefixGE call will clear boundsCmp. - // However, in this case where dontSeekWithinSingleLevelIter=true, - // we never seek on the single-level iterator. This call will fall - // through to skipForward, which may improperly leave boundsCmp=+1 - // unless we clear it here. - i.boundsCmp = 0 - } - } else { - // INVARIANT: err == nil. - // - // Else fast-path: There are two possible cases, from - // (i.boundsCmp > 0 || flags.TrySeekUsingNext()): - // - // 1) The bounds have moved forward (i.boundsCmp > 0) and this - // SeekPrefixGE is respecting the lower bound (guaranteed by Iterator). We - // know that the iterator must already be positioned within or just - // outside the previous bounds. Therefore, the topLevelIndex iter cannot - // be positioned at an entry ahead of the seek position (though it can be - // positioned behind). The !i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 - // confirms that it is not behind. Since it is not ahead and not behind it - // must be at the right position. - // - // 2) This SeekPrefixGE will land on a key that is greater than the key we - // are currently at (guaranteed by trySeekUsingNext), but since i.cmp(key, - // i.topLevelIndex.Key().UserKey) <= 0, we are at the correct lower level - // index block. No need to reset the state of singleLevelIterator. - // - // Note that cases 1 and 2 never overlap, and one of them must be true. - // This invariant checking is important enough that we do not gate it - // behind invariants.Enabled. - if i.boundsCmp > 0 == flags.TrySeekUsingNext() { - panic(fmt.Sprintf("inconsistency in optimization case 1 %t and case 2 %t", - i.boundsCmp > 0, flags.TrySeekUsingNext())) - } - - if !flags.TrySeekUsingNext() { - // Case 1. Bounds have changed so the previous exhausted bounds state is - // irrelevant. - // WARNING-data-exhausted: this is safe to do only because the monotonic - // bounds optimizations only work when !data-exhausted. If they also - // worked with data-exhausted, we have made it unclear whether - // data-exhausted is actually true. See the comment at the top of the - // file. - i.exhaustedBounds = 0 - } - // Else flags.TrySeekUsingNext(). The i.exhaustedBounds is important to - // preserve for singleLevelIterator, and twoLevelIterator.skipForward. See - // bug https://github.com/cockroachdb/pebble/issues/2036. - } - - if !dontSeekWithinSingleLevelIter { - if ikey, val := i.singleLevelIterator.seekPrefixGE( - prefix, key, flags, false /* checkFilter */); ikey != nil { - return ikey, val - } - } - // NB: skipForward checks whether exhaustedBounds is already +1. - return i.skipForward() -} - -// SeekLT implements internalIterator.SeekLT, as documented in the pebble -// package. Note that SeekLT only checks the lower bound. It is up to the -// caller to ensure that key is less than the upper bound. -func (i *twoLevelIterator) SeekLT( - key []byte, flags base.SeekLTFlags, -) (*InternalKey, base.LazyValue) { - i.exhaustedBounds = 0 - i.err = nil // clear cached iteration error - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - var result loadBlockResult - var ikey *InternalKey - // NB: Unlike SeekGE, we don't have a fast-path here since we don't know - // whether the topLevelIndex is positioned after the position that would - // be returned by doing i.topLevelIndex.SeekGE(). To know this we would - // need to know the index key preceding the current one. - // NB: If a bound-limited block property filter is configured, it's - // externally ensured that the filter is disabled (through returning - // Intersects=false irrespective of the block props provided) during seeks. - i.maybeFilteredKeysTwoLevel = false - if ikey, _ = i.topLevelIndex.SeekGE(key, base.SeekGEFlagsNone); ikey == nil { - if ikey, _ = i.topLevelIndex.Last(); ikey == nil { - i.data.invalidate() - i.index.invalidate() - return nil, base.LazyValue{} - } - - result = i.loadIndex(-1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.singleLevelIterator.lastInternal(); ikey != nil { - return ikey, val - } - // Fall through to skipBackward since the singleLevelIterator did - // not have any blocks that satisfy the block interval - // constraints, or the lower bound was reached. - } - // Else loadBlockIrrelevant, so fall through. - } else { - result = i.loadIndex(-1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.singleLevelIterator.SeekLT(key, flags); ikey != nil { - return ikey, val - } - // Fall through to skipBackward since the singleLevelIterator did - // not have any blocks that satisfy the block interval - // constraint, or the lower bound was reached. - } - // Else loadBlockIrrelevant, so fall through. - } - if result == loadBlockIrrelevant { - // Enforce the lower bound here since don't want to bother moving to - // the previous entry in the top level index if lower bound is already - // exceeded. Note that the previous entry starts with keys <= - // ikey.UserKey since even though this is the current block's - // separator, the same user key can span multiple index blocks. - if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { - i.exhaustedBounds = -1 - } - } - // NB: skipBackward checks whether exhaustedBounds is already -1. - return i.skipBackward() -} - -// First implements internalIterator.First, as documented in the pebble -// package. Note that First only checks the upper bound. It is up to the caller -// to ensure that key is greater than or equal to the lower bound (e.g. via a -// call to SeekGE(lower)). -func (i *twoLevelIterator) First() (*InternalKey, base.LazyValue) { - if i.lower != nil { - panic("twoLevelIterator.First() used despite lower bound") - } - i.exhaustedBounds = 0 - i.maybeFilteredKeysTwoLevel = false - i.err = nil // clear cached iteration error - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - var ikey *InternalKey - if ikey, _ = i.topLevelIndex.First(); ikey == nil { - return nil, base.LazyValue{} - } - - result := i.loadIndex(+1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.singleLevelIterator.First(); ikey != nil { - return ikey, val - } - // Else fall through to skipForward. - } else { - // result == loadBlockIrrelevant. Enforce the upper bound here since - // don't want to bother moving to the next entry in the top level - // index if upper bound is already exceeded. Note that the next entry - // starts with keys >= ikey.UserKey since even though this is the - // block separator, the same user key can span multiple index blocks. - // Since upper is exclusive we use >= below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - } - } - // NB: skipForward checks whether exhaustedBounds is already +1. - return i.skipForward() -} - -// Last implements internalIterator.Last, as documented in the pebble -// package. Note that Last only checks the lower bound. It is up to the caller -// to ensure that key is less than the upper bound (e.g. via a call to -// SeekLT(upper)) -func (i *twoLevelIterator) Last() (*InternalKey, base.LazyValue) { - if i.upper != nil { - panic("twoLevelIterator.Last() used despite upper bound") - } - i.exhaustedBounds = 0 - i.maybeFilteredKeysTwoLevel = false - i.err = nil // clear cached iteration error - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - - var ikey *InternalKey - if ikey, _ = i.topLevelIndex.Last(); ikey == nil { - return nil, base.LazyValue{} - } - - result := i.loadIndex(-1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.singleLevelIterator.Last(); ikey != nil { - return ikey, val - } - // Else fall through to skipBackward. - } else { - // result == loadBlockIrrelevant. Enforce the lower bound here - // since don't want to bother moving to the previous entry in the - // top level index if lower bound is already exceeded. Note that - // the previous entry starts with keys <= ikey.UserKey since even - // though this is the current block's separator, the same user key - // can span multiple index blocks. - if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { - i.exhaustedBounds = -1 - } - } - // NB: skipBackward checks whether exhaustedBounds is already -1. - return i.skipBackward() -} - -// Next implements internalIterator.Next, as documented in the pebble -// package. -// Note: twoLevelCompactionIterator.Next mirrors the implementation of -// twoLevelIterator.Next due to performance. Keep the two in sync. -func (i *twoLevelIterator) Next() (*InternalKey, base.LazyValue) { - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - i.maybeFilteredKeysTwoLevel = false - if i.err != nil { - return nil, base.LazyValue{} - } - if key, val := i.singleLevelIterator.Next(); key != nil { - return key, val - } - return i.skipForward() -} - -// NextPrefix implements (base.InternalIterator).NextPrefix. -func (i *twoLevelIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { - if i.exhaustedBounds == +1 { - panic("Next called even though exhausted upper bound") - } - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - i.maybeFilteredKeysTwoLevel = false - if i.err != nil { - return nil, base.LazyValue{} - } - if key, val := i.singleLevelIterator.NextPrefix(succKey); key != nil { - return key, val - } - // Did not find prefix in the existing second-level index block. This is the - // slow-path where we seek the iterator. - var ikey *InternalKey - if ikey, _ = i.topLevelIndex.SeekGE(succKey, base.SeekGEFlagsNone); ikey == nil { - i.data.invalidate() - i.index.invalidate() - return nil, base.LazyValue{} - } - result := i.loadIndex(+1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockIrrelevant { - // Enforce the upper bound here since don't want to bother moving to the - // next entry in the top level index if upper bound is already exceeded. - // Note that the next entry starts with keys >= ikey.UserKey since even - // though this is the block separator, the same user key can span multiple - // index blocks. Since upper is exclusive we use >= below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - } - } else if key, val := i.singleLevelIterator.SeekGE(succKey, base.SeekGEFlagsNone); key != nil { - return key, val - } - return i.skipForward() -} - -// Prev implements internalIterator.Prev, as documented in the pebble -// package. -func (i *twoLevelIterator) Prev() (*InternalKey, base.LazyValue) { - // Seek optimization only applies until iterator is first positioned after SetBounds. - i.boundsCmp = 0 - i.maybeFilteredKeysTwoLevel = false - if i.err != nil { - return nil, base.LazyValue{} - } - if key, val := i.singleLevelIterator.Prev(); key != nil { - return key, val - } - return i.skipBackward() -} - -func (i *twoLevelIterator) skipForward() (*InternalKey, base.LazyValue) { - for { - if i.err != nil || i.exhaustedBounds > 0 { - return nil, base.LazyValue{} - } - i.exhaustedBounds = 0 - var ikey *InternalKey - if ikey, _ = i.topLevelIndex.Next(); ikey == nil { - i.data.invalidate() - i.index.invalidate() - return nil, base.LazyValue{} - } - result := i.loadIndex(+1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.singleLevelIterator.firstInternal(); ikey != nil { - return ikey, val - } - // Next iteration will return if singleLevelIterator set - // exhaustedBounds = +1. - } else { - // result == loadBlockIrrelevant. Enforce the upper bound here - // since don't want to bother moving to the next entry in the top - // level index if upper bound is already exceeded. Note that the - // next entry starts with keys >= ikey.UserKey since even though - // this is the block separator, the same user key can span - // multiple index blocks. Since upper is exclusive we use >= - // below. - if i.upper != nil && i.cmp(ikey.UserKey, i.upper) >= 0 { - i.exhaustedBounds = +1 - // Next iteration will return. - } - } - } -} - -func (i *twoLevelIterator) skipBackward() (*InternalKey, base.LazyValue) { - for { - if i.err != nil || i.exhaustedBounds < 0 { - return nil, base.LazyValue{} - } - i.exhaustedBounds = 0 - var ikey *InternalKey - if ikey, _ = i.topLevelIndex.Prev(); ikey == nil { - i.data.invalidate() - i.index.invalidate() - return nil, base.LazyValue{} - } - result := i.loadIndex(-1) - if result == loadBlockFailed { - return nil, base.LazyValue{} - } - if result == loadBlockOK { - if ikey, val := i.singleLevelIterator.lastInternal(); ikey != nil { - return ikey, val - } - // Next iteration will return if singleLevelIterator set - // exhaustedBounds = -1. - } else { - // result == loadBlockIrrelevant. Enforce the lower bound here - // since don't want to bother moving to the previous entry in the - // top level index if lower bound is already exceeded. Note that - // the previous entry starts with keys <= ikey.UserKey since even - // though this is the current block's separator, the same user key - // can span multiple index blocks. - if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { - i.exhaustedBounds = -1 - // Next iteration will return. - } - } - } -} - -// Close implements internalIterator.Close, as documented in the pebble -// package. -func (i *twoLevelIterator) Close() error { - var err error - if i.closeHook != nil { - err = firstError(err, i.closeHook(i)) - } - err = firstError(err, i.data.Close()) - err = firstError(err, i.index.Close()) - err = firstError(err, i.topLevelIndex.Close()) - if i.dataRH != nil { - err = firstError(err, i.dataRH.Close()) - i.dataRH = nil - } - err = firstError(err, i.err) - if i.bpfs != nil { - releaseBlockPropertiesFilterer(i.bpfs) - } - if i.vbReader != nil { - i.vbReader.close() - } - if i.vbRH != nil { - err = firstError(err, i.vbRH.Close()) - i.vbRH = nil - } - *i = twoLevelIterator{ - singleLevelIterator: i.singleLevelIterator.resetForReuse(), - topLevelIndex: i.topLevelIndex.resetForReuse(), - } - twoLevelIterPool.Put(i) - return err -} - -// Note: twoLevelCompactionIterator and compactionIterator are very similar but -// were separated due to performance. -type twoLevelCompactionIterator struct { - *twoLevelIterator - bytesIterated *uint64 - prevOffset uint64 -} - -// twoLevelCompactionIterator implements the base.InternalIterator interface. -var _ base.InternalIterator = (*twoLevelCompactionIterator)(nil) - -func (i *twoLevelCompactionIterator) Close() error { - return i.twoLevelIterator.Close() -} - -func (i *twoLevelCompactionIterator) SeekGE( - key []byte, flags base.SeekGEFlags, -) (*InternalKey, base.LazyValue) { - panic("pebble: SeekGE unimplemented") -} - -func (i *twoLevelCompactionIterator) SeekPrefixGE( - prefix, key []byte, flags base.SeekGEFlags, -) (*base.InternalKey, base.LazyValue) { - panic("pebble: SeekPrefixGE unimplemented") -} - -func (i *twoLevelCompactionIterator) SeekLT( - key []byte, flags base.SeekLTFlags, -) (*InternalKey, base.LazyValue) { - panic("pebble: SeekLT unimplemented") -} - -func (i *twoLevelCompactionIterator) First() (*InternalKey, base.LazyValue) { - i.err = nil // clear cached iteration error - return i.skipForward(i.twoLevelIterator.First()) -} - -func (i *twoLevelCompactionIterator) Last() (*InternalKey, base.LazyValue) { - panic("pebble: Last unimplemented") -} - -// Note: twoLevelCompactionIterator.Next mirrors the implementation of -// twoLevelIterator.Next due to performance. Keep the two in sync. -func (i *twoLevelCompactionIterator) Next() (*InternalKey, base.LazyValue) { - if i.err != nil { - return nil, base.LazyValue{} - } - return i.skipForward(i.singleLevelIterator.Next()) -} - -func (i *twoLevelCompactionIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { - panic("pebble: NextPrefix unimplemented") -} - -func (i *twoLevelCompactionIterator) Prev() (*InternalKey, base.LazyValue) { - panic("pebble: Prev unimplemented") -} - -func (i *twoLevelCompactionIterator) String() string { - return i.reader.fileNum.String() -} - -func (i *twoLevelCompactionIterator) skipForward( - key *InternalKey, val base.LazyValue, -) (*InternalKey, base.LazyValue) { - if key == nil { - for { - if key, _ := i.topLevelIndex.Next(); key == nil { - break - } - result := i.loadIndex(+1) - if result != loadBlockOK { - if i.err != nil { - break - } - switch result { - case loadBlockFailed: - // We checked that i.index was at a valid entry, so - // loadBlockFailed could not have happened due to to i.index - // being exhausted, and must be due to an error. - panic("loadBlock should not have failed with no error") - case loadBlockIrrelevant: - panic("compactionIter should not be using block intervals for skipping") - default: - panic(fmt.Sprintf("unexpected case %d", result)) - } - } - // result == loadBlockOK - if key, val = i.singleLevelIterator.First(); key != nil { - break - } - } - } - - curOffset := i.recordOffset() - *i.bytesIterated += uint64(curOffset - i.prevOffset) - i.prevOffset = curOffset - return key, val -} - type blockTransform func([]byte) ([]byte, error) // ReaderOption provide an interface to do work on Reader while it is being @@ -2551,7 +129,7 @@ func (m Mergers) readerApply(r *Reader) { // number. If not specified, a unique cache ID will be used. type cacheOpts struct { cacheID uint64 - fileNum base.FileNum + fileNum base.DiskFileNum } // Marker function to indicate the option should be applied before reading the @@ -2563,7 +141,7 @@ func (c *cacheOpts) readerApply(r *Reader) { if r.cacheID == 0 { r.cacheID = c.cacheID } - if r.fileNum == 0 { + if r.fileNum.FileNum() == 0 { r.fileNum = c.fileNum } } @@ -2572,7 +150,7 @@ func (c *cacheOpts) writerApply(w *Writer) { if w.cacheID == 0 { w.cacheID = c.cacheID } - if w.fileNum == 0 { + if w.fileNum.FileNum() == 0 { w.fileNum = c.fileNum } } @@ -2590,17 +168,39 @@ func (rawTombstonesOpt) readerApply(r *Reader) { } func init() { - private.SSTableCacheOpts = func(cacheID uint64, fileNum base.FileNum) interface{} { + private.SSTableCacheOpts = func(cacheID uint64, fileNum base.DiskFileNum) interface{} { return &cacheOpts{cacheID, fileNum} } private.SSTableRawTombstonesOpt = rawTombstonesOpt{} } +// CommonReader abstracts functionality over a Reader or a VirtualReader. This +// can be used by code which doesn't care to distinguish between a reader and a +// virtual reader. +type CommonReader interface { + NewRawRangeKeyIter() (keyspan.FragmentIterator, error) + NewRawRangeDelIter() (keyspan.FragmentIterator, error) + NewIterWithBlockPropertyFiltersAndContextEtc( + ctx context.Context, lower, upper []byte, + filterer *BlockPropertiesFilterer, + hideObsoletePoints, useFilterBlock bool, + stats *base.InternalIteratorStats, + rp ReaderProvider, + ) (Iterator, error) + NewCompactionIter( + bytesIterated *uint64, + rp ReaderProvider, + bufferPool *BufferPool, + ) (Iterator, error) + EstimateDiskUsage(start, end []byte) (uint64, error) + CommonProperties() *CommonProperties +} + // Reader is a table reader. type Reader struct { readable objstorage.Readable cacheID uint64 - fileNum base.FileNum + fileNum base.DiskFileNum err error indexBH BlockHandle filterBH BlockHandle @@ -2623,6 +223,14 @@ type Reader struct { rawTombstones bool mergerOK bool checksumType ChecksumType + // metaBufferPool is a buffer pool used exclusively when opening a table and + // loading its meta blocks. metaBufferPoolAlloc is used to batch-allocate + // the BufferPool.pool slice as a part of the Reader allocation. It's + // capacity 3 to accommodate the meta block (1), and both the compressed + // properties block (1) and decompressed properties block (1) + // simultaneously. + metaBufferPool BufferPool + metaBufferPoolAlloc [3]allocedBuffer } // Close implements DB.Close, as documented in the pebble package. @@ -2652,28 +260,64 @@ func (r *Reader) NewIterWithBlockPropertyFilters( stats *base.InternalIteratorStats, rp ReaderProvider, ) (Iterator, error) { - return r.NewIterWithBlockPropertyFiltersAndContext(context.Background(), lower, upper, filterer, - useFilterBlock, stats, rp) + return r.newIterWithBlockPropertyFiltersAndContext( + context.Background(), + lower, upper, filterer, false, useFilterBlock, stats, rp, nil, + ) } -// NewIterWithBlockPropertyFiltersAndContext is similar to +// NewIterWithBlockPropertyFiltersAndContextEtc is similar to // NewIterWithBlockPropertyFilters and additionally accepts a context for // tracing. -func (r *Reader) NewIterWithBlockPropertyFiltersAndContext( +// +// If hideObsoletePoints, the callee assumes that filterer already includes +// obsoleteKeyBlockPropertyFilter. The caller can satisfy this contract by +// first calling TryAddBlockPropertyFilterForHideObsoletePoints. +func (r *Reader) NewIterWithBlockPropertyFiltersAndContextEtc( ctx context.Context, lower, upper []byte, filterer *BlockPropertiesFilterer, - useFilterBlock bool, + hideObsoletePoints, useFilterBlock bool, stats *base.InternalIteratorStats, rp ReaderProvider, ) (Iterator, error) { + return r.newIterWithBlockPropertyFiltersAndContext( + ctx, lower, upper, filterer, hideObsoletePoints, useFilterBlock, stats, rp, nil, + ) +} +// TryAddBlockPropertyFilterForHideObsoletePoints is expected to be called +// before the call to NewIterWithBlockPropertyFiltersAndContextEtc, to get the +// value of hideObsoletePoints and potentially add a block property filter. +func (r *Reader) TryAddBlockPropertyFilterForHideObsoletePoints( + snapshotForHideObsoletePoints uint64, + fileLargestSeqNum uint64, + pointKeyFilters []BlockPropertyFilter, +) (hideObsoletePoints bool, filters []BlockPropertyFilter) { + hideObsoletePoints = r.tableFormat >= TableFormatPebblev4 && + snapshotForHideObsoletePoints > fileLargestSeqNum + if hideObsoletePoints { + pointKeyFilters = append(pointKeyFilters, obsoleteKeyBlockPropertyFilter{}) + } + return hideObsoletePoints, pointKeyFilters +} + +func (r *Reader) newIterWithBlockPropertyFiltersAndContext( + ctx context.Context, + lower, upper []byte, + filterer *BlockPropertiesFilterer, + hideObsoletePoints bool, + useFilterBlock bool, + stats *base.InternalIteratorStats, + rp ReaderProvider, + v *virtualState, +) (Iterator, error) { // NB: pebble.tableCache wraps the returned iterator with one which performs // reference counting on the Reader, preventing the Reader from being closed // until the final iterator closes. if r.Properties.IndexType == twoLevelIndex { i := twoLevelIterPool.Get().(*twoLevelIterator) - err := i.init(ctx, r, lower, upper, filterer, useFilterBlock, stats, rp) + err := i.init(ctx, r, v, lower, upper, filterer, useFilterBlock, hideObsoletePoints, stats, rp, nil /* bufferPool */) if err != nil { return nil, err } @@ -2681,7 +325,7 @@ func (r *Reader) NewIterWithBlockPropertyFiltersAndContext( } i := singleLevelIterPool.Get().(*singleLevelIterator) - err := i.init(ctx, r, lower, upper, filterer, useFilterBlock, stats, rp) + err := i.init(ctx, r, v, lower, upper, filterer, useFilterBlock, hideObsoletePoints, stats, rp, nil /* bufferPool */) if err != nil { return nil, err } @@ -2701,11 +345,23 @@ func (r *Reader) NewIter(lower, upper []byte) (Iterator, error) { // NewCompactionIter returns an iterator similar to NewIter but it also increments // the number of bytes iterated. If an error occurs, NewCompactionIter cleans up // after itself and returns a nil iterator. -func (r *Reader) NewCompactionIter(bytesIterated *uint64, rp ReaderProvider) (Iterator, error) { +func (r *Reader) NewCompactionIter( + bytesIterated *uint64, rp ReaderProvider, bufferPool *BufferPool, +) (Iterator, error) { + return r.newCompactionIter(bytesIterated, rp, nil, bufferPool) +} + +func (r *Reader) newCompactionIter( + bytesIterated *uint64, rp ReaderProvider, v *virtualState, bufferPool *BufferPool, +) (Iterator, error) { if r.Properties.IndexType == twoLevelIndex { i := twoLevelIterPool.Get().(*twoLevelIterator) - err := i.init(context.Background(), r, nil /* lower */, nil, /* upper */ - nil, false /* useFilter */, nil /* stats */, rp) + err := i.init( + context.Background(), + r, v, nil /* lower */, nil /* upper */, nil, + false /* useFilter */, v != nil && v.isForeign, /* hideObsoletePoints */ + nil /* stats */, rp, bufferPool, + ) if err != nil { return nil, err } @@ -2716,8 +372,11 @@ func (r *Reader) NewCompactionIter(bytesIterated *uint64, rp ReaderProvider) (It }, nil } i := singleLevelIterPool.Get().(*singleLevelIterator) - err := i.init(context.Background(), r, nil /* lower */, nil, /* upper */ - nil, false /* useFilter */, nil /* stats */, rp) + err := i.init( + context.Background(), r, v, nil /* lower */, nil, /* upper */ + nil, false /* useFilter */, v != nil && v.isForeign, /* hideObsoletePoints */ + nil /* stats */, rp, bufferPool, + ) if err != nil { return nil, err } @@ -2742,8 +401,8 @@ func (r *Reader) NewRawRangeDelIter() (keyspan.FragmentIterator, error) { if err != nil { return nil, err } - i := &fragmentBlockIter{} - if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum); err != nil { + i := &fragmentBlockIter{elideSameSeqnum: true} + if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum, false); err != nil { return nil, err } return i, nil @@ -2764,7 +423,7 @@ func (r *Reader) NewRawRangeKeyIter() (keyspan.FragmentIterator, error) { return nil, err } i := rangeKeyFragmentBlockIterPool.Get().(*rangeKeyFragmentBlockIter) - if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum); err != nil { + if err := i.blockIter.initHandle(r.Compare, h, r.Properties.GlobalSeqNum, false); err != nil { return nil, err } return i, nil @@ -2783,24 +442,26 @@ func (i *rangeKeyFragmentBlockIter) Close() error { func (r *Reader) readIndex( ctx context.Context, stats *base.InternalIteratorStats, -) (cache.Handle, error) { - return r.readBlock(ctx, r.indexBH, nil, nil, stats) +) (bufferHandle, error) { + ctx = objiotracing.WithBlockType(ctx, objiotracing.MetadataBlock) + return r.readBlock(ctx, r.indexBH, nil, nil, stats, nil /* buffer pool */) } func (r *Reader) readFilter( ctx context.Context, stats *base.InternalIteratorStats, -) (cache.Handle, error) { - return r.readBlock(ctx, r.filterBH, nil /* transform */, nil /* readHandle */, stats) +) (bufferHandle, error) { + ctx = objiotracing.WithBlockType(ctx, objiotracing.FilterBlock) + return r.readBlock(ctx, r.filterBH, nil /* transform */, nil /* readHandle */, stats, nil /* buffer pool */) } -func (r *Reader) readRangeDel(stats *base.InternalIteratorStats) (cache.Handle, error) { - return r.readBlock( - context.Background(), r.rangeDelBH, r.rangeDelTransform, nil /* readHandle */, stats) +func (r *Reader) readRangeDel(stats *base.InternalIteratorStats) (bufferHandle, error) { + ctx := objiotracing.WithBlockType(context.Background(), objiotracing.MetadataBlock) + return r.readBlock(ctx, r.rangeDelBH, r.rangeDelTransform, nil /* readHandle */, stats, nil /* buffer pool */) } -func (r *Reader) readRangeKey(stats *base.InternalIteratorStats) (cache.Handle, error) { - return r.readBlock( - context.Background(), r.rangeKeyBH, nil /* transform */, nil /* readHandle */, stats) +func (r *Reader) readRangeKey(stats *base.InternalIteratorStats) (bufferHandle, error) { + ctx := objiotracing.WithBlockType(context.Background(), objiotracing.MetadataBlock) + return r.readBlock(ctx, r.rangeKeyBH, nil /* transform */, nil /* readHandle */, stats, nil /* buffer pool */) } func checkChecksum( @@ -2825,33 +486,76 @@ func checkChecksum( return nil } -// readBlock reads and decompresses a block from disk into memory. +type cacheValueOrBuf struct { + // buf.Valid() returns true if backed by a BufferPool. + buf Buf + // v is non-nil if backed by the block cache. + v *cache.Value +} + +func (b cacheValueOrBuf) get() []byte { + if b.buf.Valid() { + return b.buf.p.pool[b.buf.i].b + } + return b.v.Buf() +} + +func (b cacheValueOrBuf) release() { + if b.buf.Valid() { + b.buf.Release() + } else { + cache.Free(b.v) + } +} + +func (b cacheValueOrBuf) truncate(n int) { + if b.buf.Valid() { + b.buf.p.pool[b.buf.i].b = b.buf.p.pool[b.buf.i].b[:n] + } else { + b.v.Truncate(n) + } +} + func (r *Reader) readBlock( ctx context.Context, bh BlockHandle, transform blockTransform, readHandle objstorage.ReadHandle, stats *base.InternalIteratorStats, -) (handle cache.Handle, _ error) { + bufferPool *BufferPool, +) (handle bufferHandle, _ error) { if h := r.opts.Cache.Get(r.cacheID, r.fileNum, bh.Offset); h.Get() != nil { + // Cache hit. if readHandle != nil { - readHandle.RecordCacheHit(int64(bh.Offset), int64(bh.Length+blockTrailerLen)) + readHandle.RecordCacheHit(ctx, int64(bh.Offset), int64(bh.Length+blockTrailerLen)) } if stats != nil { stats.BlockBytes += bh.Length stats.BlockBytesInCache += bh.Length } - return h, nil + // This block is already in the cache; return a handle to existing vlaue + // in the cache. + return bufferHandle{h: h}, nil + } + + // Cache miss. + var compressed cacheValueOrBuf + if bufferPool != nil { + compressed = cacheValueOrBuf{ + buf: bufferPool.Alloc(int(bh.Length + blockTrailerLen)), + } + } else { + compressed = cacheValueOrBuf{ + v: cache.Alloc(int(bh.Length + blockTrailerLen)), + } } - v := r.opts.Cache.Alloc(int(bh.Length + blockTrailerLen)) - b := v.Buf() readStartTime := time.Now() var err error if readHandle != nil { - _, err = readHandle.ReadAt(b, int64(bh.Offset)) + err = readHandle.ReadAt(ctx, compressed.get(), int64(bh.Offset)) } else { - _, err = r.readable.ReadAt(b, int64(bh.Offset)) + err = r.readable.ReadAt(ctx, compressed.get(), int64(bh.Offset)) } readDuration := time.Since(readStartTime) // TODO(sumeer): should the threshold be configurable. @@ -2864,56 +568,74 @@ func (r *Reader) readBlock( // interface{}, unless necessary. if readDuration >= slowReadTracingThreshold && r.opts.LoggerAndTracer.IsTracingEnabled(ctx) { r.opts.LoggerAndTracer.Eventf(ctx, "reading %d bytes took %s", - bh.Length+blockTrailerLen, readDuration.String()) + int(bh.Length+blockTrailerLen), readDuration.String()) } if stats != nil { stats.BlockReadDuration += readDuration } if err != nil { - r.opts.Cache.Free(v) - return cache.Handle{}, err + compressed.release() + return bufferHandle{}, err + } + if err := checkChecksum(r.checksumType, compressed.get(), bh, r.fileNum.FileNum()); err != nil { + compressed.release() + return bufferHandle{}, err } - if err := checkChecksum(r.checksumType, b, bh, r.fileNum); err != nil { - r.opts.Cache.Free(v) - return cache.Handle{}, err - } + typ := blockType(compressed.get()[bh.Length]) + compressed.truncate(int(bh.Length)) - typ := blockType(b[bh.Length]) - b = b[:bh.Length] - v.Truncate(len(b)) + var decompressed cacheValueOrBuf + if typ == noCompressionBlockType { + decompressed = compressed + } else { + // Decode the length of the decompressed value. + decodedLen, prefixLen, err := decompressedLen(typ, compressed.get()) + if err != nil { + compressed.release() + return bufferHandle{}, err + } - decoded, err := decompressBlock(r.opts.Cache, typ, b) - if decoded != nil { - r.opts.Cache.Free(v) - v = decoded - b = v.Buf() - } else if err != nil { - r.opts.Cache.Free(v) - return cache.Handle{}, err + if bufferPool != nil { + decompressed = cacheValueOrBuf{buf: bufferPool.Alloc(decodedLen)} + } else { + decompressed = cacheValueOrBuf{v: cache.Alloc(decodedLen)} + } + if _, err := decompressInto(typ, compressed.get()[prefixLen:], decompressed.get()); err != nil { + compressed.release() + return bufferHandle{}, err + } + compressed.release() } if transform != nil { - // Transforming blocks is rare, so the extra copy of the transformed data - // is not problematic. - var err error - b, err = transform(b) + // Transforming blocks is very rare, so the extra copy of the + // transformed data is not problematic. + tmpTransformed, err := transform(decompressed.get()) if err != nil { - r.opts.Cache.Free(v) - return cache.Handle{}, err + decompressed.release() + return bufferHandle{}, err } - newV := r.opts.Cache.Alloc(len(b)) - copy(newV.Buf(), b) - r.opts.Cache.Free(v) - v = newV + + var transformed cacheValueOrBuf + if bufferPool != nil { + transformed = cacheValueOrBuf{buf: bufferPool.Alloc(len(tmpTransformed))} + } else { + transformed = cacheValueOrBuf{v: cache.Alloc(len(tmpTransformed))} + } + copy(transformed.get(), tmpTransformed) + decompressed.release() + decompressed = transformed } if stats != nil { stats.BlockBytes += bh.Length } - - h := r.opts.Cache.Set(r.cacheID, r.fileNum, bh.Offset, v) - return h, nil + if decompressed.buf.Valid() { + return bufferHandle{b: decompressed.buf}, nil + } + h := r.opts.Cache.Set(r.cacheID, r.fileNum, bh.Offset, decompressed.v) + return bufferHandle{h: h}, nil } func (r *Reader) transformRangeDelV1(b []byte) ([]byte, error) { @@ -2922,7 +644,7 @@ func (r *Reader) transformRangeDelV1(b []byte) ([]byte, error) { // tombstones. We need properly fragmented and sorted range tombstones in // order to serve from them directly. iter := &blockIter{} - if err := iter.init(r.Compare, b, r.Properties.GlobalSeqNum); err != nil { + if err := iter.init(r.Compare, b, r.Properties.GlobalSeqNum, false); err != nil { return nil, err } var tombstones []keyspan.Span @@ -2960,8 +682,21 @@ func (r *Reader) transformRangeDelV1(b []byte) ([]byte, error) { } func (r *Reader) readMetaindex(metaindexBH BlockHandle) error { + // We use a BufferPool when reading metaindex blocks in order to avoid + // populating the block cache with these blocks. In heavy-write workloads, + // especially with high compaction concurrency, new tables may be created + // frequently. Populating the block cache with these metaindex blocks adds + // additional contention on the block cache mutexes (see #1997). + // Additionally, these blocks are exceedingly unlikely to be read again + // while they're still in the block cache except in misconfigurations with + // excessive sstables counts or a table cache that's far too small. + r.metaBufferPool.initPreallocated(r.metaBufferPoolAlloc[:0]) + // When we're finished, release the buffers we've allocated back to memory + // allocator. We don't expect to use metaBufferPool again. + defer r.metaBufferPool.Release() + b, err := r.readBlock( - context.Background(), metaindexBH, nil /* transform */, nil /* readHandle */, nil /* stats */) + context.Background(), metaindexBH, nil /* transform */, nil /* readHandle */, nil /* stats */, &r.metaBufferPool) if err != nil { return err } @@ -3004,12 +739,12 @@ func (r *Reader) readMetaindex(metaindexBH BlockHandle) error { if bh, ok := meta[metaPropertiesName]; ok { b, err = r.readBlock( - context.Background(), bh, nil /* transform */, nil /* readHandle */, nil /* stats */) + context.Background(), bh, nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* buffer pool */) if err != nil { return err } r.propertiesBH = bh - err := r.Properties.load(b.Get(), bh.Offset) + err := r.Properties.load(b.Get(), bh.Offset, r.opts.DeniedUserProperties) b.Release() if err != nil { return err @@ -3109,12 +844,13 @@ func (r *Reader) Layout() (*Layout, error) { } l.Index = append(l.Index, indexBH.BlockHandle) - subIndex, err := r.readBlock(context.Background(), - indexBH.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */) + subIndex, err := r.readBlock(context.Background(), indexBH.BlockHandle, + nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* buffer pool */) if err != nil { return nil, err } - if err := iter.init(r.Compare, subIndex.Get(), 0 /* globalSeqNum */); err != nil { + if err := iter.init(r.Compare, subIndex.Get(), 0, /* globalSeqNum */ + false /* hideObsoletePoints */); err != nil { return nil, err } for key, value := iter.First(); key != nil; key, value = iter.Next() { @@ -3132,7 +868,7 @@ func (r *Reader) Layout() (*Layout, error) { } } if r.valueBIH.h.Length != 0 { - vbiH, err := r.readBlock(context.Background(), r.valueBIH.h, nil, nil, nil) + vbiH, err := r.readBlock(context.Background(), r.valueBIH.h, nil, nil, nil, nil /* buffer pool */) if err != nil { return nil, err } @@ -3193,7 +929,7 @@ func (r *Reader) ValidateBlockChecksums() error { // Check all blocks sequentially. Make use of read-ahead, given we are // scanning the entire file from start to end. - rh := r.readable.NewReadHandle() + rh := r.readable.NewReadHandle(context.TODO()) defer rh.Close() for _, bh := range blocks { @@ -3203,7 +939,7 @@ func (r *Reader) ValidateBlockChecksums() error { } // Read the block, which validates the checksum. - h, err := r.readBlock(context.Background(), bh, nil, rh, nil) + h, err := r.readBlock(context.Background(), bh, nil, rh, nil, nil /* buffer pool */) if err != nil { return err } @@ -3213,6 +949,11 @@ func (r *Reader) ValidateBlockChecksums() error { return nil } +// CommonProperties implemented the CommonReader interface. +func (r *Reader) CommonProperties() *CommonProperties { + return &r.Properties.CommonProperties +} + // EstimateDiskUsage returns the total size of data blocks overlapping the range // `[start, end]`. Even if a data block partially overlaps, or we cannot // determine overlap due to abbreviated index keys, the full data block size is @@ -3265,8 +1006,8 @@ func (r *Reader) EstimateDiskUsage(start, end []byte) (uint64, error) { if err != nil { return 0, errCorruptIndexEntry } - startIdxBlock, err := r.readBlock(context.Background(), - startIdxBH.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */) + startIdxBlock, err := r.readBlock(context.Background(), startIdxBH.BlockHandle, + nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* buffer pool */) if err != nil { return 0, err } @@ -3287,7 +1028,7 @@ func (r *Reader) EstimateDiskUsage(start, end []byte) (uint64, error) { return 0, errCorruptIndexEntry } endIdxBlock, err := r.readBlock(context.Background(), - endIdxBH.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */) + endIdxBH.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */, nil /* buffer pool */) if err != nil { return 0, err } @@ -3432,299 +1173,12 @@ func NewReader(f objstorage.Readable, o ReaderOptions, extraOpts ...ReaderOption if r.err != nil { return nil, r.Close() } + return r, nil } -// Layout describes the block organization of an sstable. -type Layout struct { - // NOTE: changes to fields in this struct should also be reflected in - // ValidateBlockChecksums, which validates a static list of BlockHandles - // referenced in this struct. - - Data []BlockHandleWithProperties - Index []BlockHandle - TopIndex BlockHandle - Filter BlockHandle - RangeDel BlockHandle - RangeKey BlockHandle - ValueBlock []BlockHandle - ValueIndex BlockHandle - Properties BlockHandle - MetaIndex BlockHandle - Footer BlockHandle - Format TableFormat -} - -// Describe returns a description of the layout. If the verbose parameter is -// true, details of the structure of each block are returned as well. -func (l *Layout) Describe( - w io.Writer, verbose bool, r *Reader, fmtRecord func(key *base.InternalKey, value []byte), -) { - type block struct { - BlockHandle - name string - } - var blocks []block - - for i := range l.Data { - blocks = append(blocks, block{l.Data[i].BlockHandle, "data"}) - } - for i := range l.Index { - blocks = append(blocks, block{l.Index[i], "index"}) - } - if l.TopIndex.Length != 0 { - blocks = append(blocks, block{l.TopIndex, "top-index"}) - } - if l.Filter.Length != 0 { - blocks = append(blocks, block{l.Filter, "filter"}) - } - if l.RangeDel.Length != 0 { - blocks = append(blocks, block{l.RangeDel, "range-del"}) - } - if l.RangeKey.Length != 0 { - blocks = append(blocks, block{l.RangeKey, "range-key"}) - } - for i := range l.ValueBlock { - blocks = append(blocks, block{l.ValueBlock[i], "value-block"}) - } - if l.ValueIndex.Length != 0 { - blocks = append(blocks, block{l.ValueIndex, "value-index"}) - } - if l.Properties.Length != 0 { - blocks = append(blocks, block{l.Properties, "properties"}) - } - if l.MetaIndex.Length != 0 { - blocks = append(blocks, block{l.MetaIndex, "meta-index"}) - } - if l.Footer.Length != 0 { - if l.Footer.Length == levelDBFooterLen { - blocks = append(blocks, block{l.Footer, "leveldb-footer"}) - } else { - blocks = append(blocks, block{l.Footer, "footer"}) - } - } - - sort.Slice(blocks, func(i, j int) bool { - return blocks[i].Offset < blocks[j].Offset - }) - - for i := range blocks { - b := &blocks[i] - fmt.Fprintf(w, "%10d %s (%d)\n", b.Offset, b.name, b.Length) - - if !verbose { - continue - } - if b.name == "filter" { - continue - } - - if b.name == "footer" || b.name == "leveldb-footer" { - trailer, offset := make([]byte, b.Length), b.Offset - _, _ = r.readable.ReadAt(trailer, int64(offset)) - - if b.name == "footer" { - checksumType := ChecksumType(trailer[0]) - fmt.Fprintf(w, "%10d checksum type: %s\n", offset, checksumType) - trailer, offset = trailer[1:], offset+1 - } - - metaHandle, n := binary.Uvarint(trailer) - metaLen, m := binary.Uvarint(trailer[n:]) - fmt.Fprintf(w, "%10d meta: offset=%d, length=%d\n", offset, metaHandle, metaLen) - trailer, offset = trailer[n+m:], offset+uint64(n+m) - - indexHandle, n := binary.Uvarint(trailer) - indexLen, m := binary.Uvarint(trailer[n:]) - fmt.Fprintf(w, "%10d index: offset=%d, length=%d\n", offset, indexHandle, indexLen) - trailer, offset = trailer[n+m:], offset+uint64(n+m) - - fmt.Fprintf(w, "%10d [padding]\n", offset) - - trailing := 12 - if b.name == "leveldb-footer" { - trailing = 8 - } - - offset += uint64(len(trailer) - trailing) - trailer = trailer[len(trailer)-trailing:] - - if b.name == "footer" { - version := trailer[:4] - fmt.Fprintf(w, "%10d version: %d\n", offset, binary.LittleEndian.Uint32(version)) - trailer, offset = trailer[4:], offset+4 - } - - magicNumber := trailer - fmt.Fprintf(w, "%10d magic number: 0x%x\n", offset, magicNumber) - - continue - } - - h, err := r.readBlock( - context.Background(), b.BlockHandle, nil /* transform */, nil /* readHandle */, nil /* stats */) - if err != nil { - fmt.Fprintf(w, " [err: %s]\n", err) - continue - } - - getRestart := func(data []byte, restarts, i int32) int32 { - return decodeRestart(data[restarts+4*i:]) - } - - formatIsRestart := func(data []byte, restarts, numRestarts, offset int32) { - i := sort.Search(int(numRestarts), func(i int) bool { - return getRestart(data, restarts, int32(i)) >= offset - }) - if i < int(numRestarts) && getRestart(data, restarts, int32(i)) == offset { - fmt.Fprintf(w, " [restart]\n") - } else { - fmt.Fprintf(w, "\n") - } - } - - formatRestarts := func(data []byte, restarts, numRestarts int32) { - for i := int32(0); i < numRestarts; i++ { - offset := getRestart(data, restarts, i) - fmt.Fprintf(w, "%10d [restart %d]\n", - b.Offset+uint64(restarts+4*i), b.Offset+uint64(offset)) - } - } - - formatTrailer := func() { - trailer := make([]byte, blockTrailerLen) - offset := int64(b.Offset + b.Length) - _, _ = r.readable.ReadAt(trailer, offset) - bt := blockType(trailer[0]) - checksum := binary.LittleEndian.Uint32(trailer[1:]) - fmt.Fprintf(w, "%10d [trailer compression=%s checksum=0x%04x]\n", offset, bt, checksum) - } - - var lastKey InternalKey - switch b.name { - case "data", "range-del", "range-key": - iter, _ := newBlockIter(r.Compare, h.Get()) - for key, value := iter.First(); key != nil; key, value = iter.Next() { - ptr := unsafe.Pointer(uintptr(iter.ptr) + uintptr(iter.offset)) - shared, ptr := decodeVarint(ptr) - unshared, ptr := decodeVarint(ptr) - value2, _ := decodeVarint(ptr) - - total := iter.nextOffset - iter.offset - // The format of the numbers in the record line is: - // - // ( = [] + + ) - // - // is the total number of bytes for the record. - // is the size of the 3 varint encoded integers for , - // , and . - // is the number of key bytes shared with the previous key. - // is the number of unshared key bytes. - // is the number of value bytes. - fmt.Fprintf(w, "%10d record (%d = %d [%d] + %d + %d)", - b.Offset+uint64(iter.offset), total, - total-int32(unshared+value2), shared, unshared, value2) - formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) - if fmtRecord != nil { - fmt.Fprintf(w, " ") - if l.Format != TableFormatPebblev3 { - fmtRecord(key, value.InPlaceValue()) - } else { - // InPlaceValue() will succeed even for data blocks where the - // actual value is in a different location, since this value was - // fetched from a blockIter which does not know about value - // blocks. - v := value.InPlaceValue() - if base.TrailerKind(key.Trailer) != InternalKeyKindSet { - fmtRecord(key, v) - } else if !isValueHandle(valuePrefix(v[0])) { - fmtRecord(key, v[1:]) - } else { - vh := decodeValueHandle(v[1:]) - fmtRecord(key, []byte(fmt.Sprintf("value handle %+v", vh))) - } - } - } - - if base.InternalCompare(r.Compare, lastKey, *key) >= 0 { - fmt.Fprintf(w, " WARNING: OUT OF ORDER KEYS!\n") - } - lastKey.Trailer = key.Trailer - lastKey.UserKey = append(lastKey.UserKey[:0], key.UserKey...) - } - formatRestarts(iter.data, iter.restarts, iter.numRestarts) - formatTrailer() - case "index", "top-index": - iter, _ := newBlockIter(r.Compare, h.Get()) - for key, value := iter.First(); key != nil; key, value = iter.Next() { - bh, err := decodeBlockHandleWithProperties(value.InPlaceValue()) - if err != nil { - fmt.Fprintf(w, "%10d [err: %s]\n", b.Offset+uint64(iter.offset), err) - continue - } - fmt.Fprintf(w, "%10d block:%d/%d", - b.Offset+uint64(iter.offset), bh.Offset, bh.Length) - formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) - } - formatRestarts(iter.data, iter.restarts, iter.numRestarts) - formatTrailer() - case "properties": - iter, _ := newRawBlockIter(r.Compare, h.Get()) - for valid := iter.First(); valid; valid = iter.Next() { - fmt.Fprintf(w, "%10d %s (%d)", - b.Offset+uint64(iter.offset), iter.Key().UserKey, iter.nextOffset-iter.offset) - formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) - } - formatRestarts(iter.data, iter.restarts, iter.numRestarts) - formatTrailer() - case "meta-index": - iter, _ := newRawBlockIter(r.Compare, h.Get()) - for valid := iter.First(); valid; valid = iter.Next() { - value := iter.Value() - var bh BlockHandle - var n int - var vbih valueBlocksIndexHandle - isValueBlocksIndexHandle := false - if bytes.Equal(iter.Key().UserKey, []byte(metaValueIndexName)) { - vbih, n, err = decodeValueBlocksIndexHandle(value) - bh = vbih.h - isValueBlocksIndexHandle = true - } else { - bh, n = decodeBlockHandle(value) - } - if n == 0 || n != len(value) { - fmt.Fprintf(w, "%10d [err: %s]\n", b.Offset+uint64(iter.offset), err) - continue - } - var vbihStr string - if isValueBlocksIndexHandle { - vbihStr = fmt.Sprintf(" value-blocks-index-lengths: %d(num), %d(offset), %d(length)", - vbih.blockNumByteLength, vbih.blockOffsetByteLength, vbih.blockLengthByteLength) - } - fmt.Fprintf(w, "%10d %s block:%d/%d%s", - b.Offset+uint64(iter.offset), iter.Key().UserKey, - bh.Offset, bh.Length, vbihStr) - formatIsRestart(iter.data, iter.restarts, iter.numRestarts, iter.offset) - } - formatRestarts(iter.data, iter.restarts, iter.numRestarts) - formatTrailer() - case "value-block": - // We don't peer into the value-block since it can't be interpreted - // without the valueHandles. - case "value-index": - // We have already read the value-index to construct the list of - // value-blocks, so no need to do it again. - } - - h.Release() - } - - last := blocks[len(blocks)-1] - fmt.Fprintf(w, "%10d EOF\n", last.Offset+last.Length) -} - -// ReadableFile describes the smallest subset of objstorage.Readable that is -// required for reading SSTs. +// ReadableFile describes the smallest subset of vfs.File that is required for +// reading SSTs. type ReadableFile interface { io.ReaderAt io.Closer @@ -3738,28 +1192,43 @@ func NewSimpleReadable(r ReadableFile) (objstorage.Readable, error) { if err != nil { return nil, err } - return &simpleReadable{ - ReadableFile: r, - size: info.Size(), - rh: objstorage.MakeNoopReadHandle(r), - }, nil + res := &simpleReadable{ + f: r, + size: info.Size(), + } + res.rh = objstorage.MakeNoopReadHandle(res) + return res, nil } // simpleReadable wraps a ReadableFile to implement objstorage.Readable. type simpleReadable struct { - ReadableFile + f ReadableFile size int64 rh objstorage.NoopReadHandle } var _ objstorage.Readable = (*simpleReadable)(nil) +// ReadAt is part of the objstorage.Readable interface. +func (s *simpleReadable) ReadAt(_ context.Context, p []byte, off int64) error { + n, err := s.f.ReadAt(p, off) + if invariants.Enabled && err == nil && n != len(p) { + panic("short read") + } + return err +} + +// Close is part of the objstorage.Readable interface. +func (s *simpleReadable) Close() error { + return s.f.Close() +} + // Size is part of the objstorage.Readable interface. func (s *simpleReadable) Size() int64 { return s.size } // NewReaddHandle is part of the objstorage.Readable interface. -func (s *simpleReadable) NewReadHandle() objstorage.ReadHandle { +func (s *simpleReadable) NewReadHandle(_ context.Context) objstorage.ReadHandle { return &s.rh } diff --git a/vendor/github.com/cockroachdb/pebble/sstable/reader_iter.go b/vendor/github.com/cockroachdb/pebble/sstable/reader_iter.go new file mode 100644 index 0000000..2b5a267 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/sstable/reader_iter.go @@ -0,0 +1,291 @@ +// Copyright 2011 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 sstable + +import ( + "fmt" + "os" + "sync" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" +) + +// Iterator iterates over an entire table of data. +type Iterator interface { + base.InternalIterator + + // NextPrefix implements (base.InternalIterator).NextPrefix. + NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) + + // MaybeFilteredKeys may be called when an iterator is exhausted to indicate + // whether or not the last positioning method may have skipped any keys due + // to block-property filters. This is used by the Pebble levelIter to + // control when an iterator steps to the next sstable. + // + // MaybeFilteredKeys may always return false positives, that is it may + // return true when no keys were filtered. It should only be called when the + // iterator is exhausted. It must never return false negatives when the + // iterator is exhausted. + MaybeFilteredKeys() bool + + SetCloseHook(fn func(i Iterator) error) +} + +// Iterator positioning optimizations and singleLevelIterator and +// twoLevelIterator: +// +// An iterator is absolute positioned using one of the Seek or First or Last +// calls. After absolute positioning, there can be relative positioning done +// by stepping using Prev or Next. +// +// We implement optimizations below where an absolute positioning call can in +// some cases use the current position to do less work. To understand these, +// we first define some terms. An iterator is bounds-exhausted if the bounds +// (upper of lower) have been reached. An iterator is data-exhausted if it has +// the reached the end of the data (forward or reverse) in the sstable. A +// singleLevelIterator only knows a local-data-exhausted property since when +// it is used as part of a twoLevelIterator, the twoLevelIterator can step to +// the next lower-level index block. +// +// The bounds-exhausted property is tracked by +// singleLevelIterator.exhaustedBounds being +1 (upper bound reached) or -1 +// (lower bound reached). The same field is reused by twoLevelIterator. Either +// may notice the exhaustion of the bound and set it. Note that if +// singleLevelIterator sets this property, it is not a local property (since +// the bound has been reached regardless of whether this is in the context of +// the twoLevelIterator or not). +// +// The data-exhausted property is tracked in a more subtle manner. We define +// two predicates: +// - partial-local-data-exhausted (PLDE): +// i.data.isDataInvalidated() || !i.data.valid() +// - partial-global-data-exhausted (PGDE): +// i.index.isDataInvalidated() || !i.index.valid() || i.data.isDataInvalidated() || +// !i.data.valid() +// +// PLDE is defined for a singleLevelIterator. PGDE is defined for a +// twoLevelIterator. Oddly, in our code below the singleLevelIterator does not +// know when it is part of a twoLevelIterator so it does not know when its +// property is local or global. +// +// Now to define data-exhausted: +// - Prerequisite: we must know that the iterator has been positioned and +// i.err is nil. +// - bounds-exhausted must not be true: +// If bounds-exhausted is true, we have incomplete knowledge of +// data-exhausted since PLDE or PGDE could be true because we could have +// chosen not to load index block or data block and figured out that the +// bound is exhausted (due to block property filters filtering out index and +// data blocks and going past the bound on the top level index block). Note +// that if we tried to separate out the BPF case from others we could +// develop more knowledge here. +// - PGDE is true for twoLevelIterator. PLDE is true if it is a standalone +// singleLevelIterator. !PLDE or !PGDE of course imply that data-exhausted +// is not true. +// +// An implication of the above is that if we are going to somehow utilize +// knowledge of data-exhausted in an optimization, we must not forget the +// existing value of bounds-exhausted since by forgetting the latter we can +// erroneously think that data-exhausted is true. Bug #2036 was due to this +// forgetting. +// +// Now to the two categories of optimizations we currently have: +// - Monotonic bounds optimization that reuse prior iterator position when +// doing seek: These only work with !data-exhausted. We could choose to make +// these work with data-exhausted but have not bothered because in the +// context of a DB if data-exhausted were true, the DB would move to the +// next file in the level. Note that this behavior of moving to the next +// file is not necessarily true for L0 files, so there could be some benefit +// in the future in this optimization. See the WARNING-data-exhausted +// comments if trying to optimize this in the future. +// - TrySeekUsingNext optimizations: these work regardless of exhaustion +// state. +// +// Implementation detail: In the code PLDE only checks that +// i.data.isDataInvalidated(). This narrower check is safe, since this is a +// subset of the set expressed by the OR expression. Also, it is not a +// de-optimization since whenever we exhaust the iterator we explicitly call +// i.data.invalidate(). PGDE checks i.index.isDataInvalidated() && +// i.data.isDataInvalidated(). Again, this narrower check is safe, and not a +// de-optimization since whenever we exhaust the iterator we explicitly call +// i.index.invalidate() and i.data.invalidate(). The && is questionable -- for +// now this is a bit of defensive code. We should seriously consider removing +// it, since defensive code suggests we are not confident about our invariants +// (and if we are not confident, we need more invariant assertions, not +// defensive code). +// +// TODO(sumeer): remove the aforementioned defensive code. + +var singleLevelIterPool = sync.Pool{ + New: func() interface{} { + i := &singleLevelIterator{} + // Note: this is a no-op if invariants are disabled or race is enabled. + invariants.SetFinalizer(i, checkSingleLevelIterator) + return i + }, +} + +var twoLevelIterPool = sync.Pool{ + New: func() interface{} { + i := &twoLevelIterator{} + // Note: this is a no-op if invariants are disabled or race is enabled. + invariants.SetFinalizer(i, checkTwoLevelIterator) + return i + }, +} + +// TODO(jackson): rangedel fragmentBlockIters can't be pooled because of some +// code paths that double Close the iters. Fix the double close and pool the +// *fragmentBlockIter type directly. + +var rangeKeyFragmentBlockIterPool = sync.Pool{ + New: func() interface{} { + i := &rangeKeyFragmentBlockIter{} + // Note: this is a no-op if invariants are disabled or race is enabled. + invariants.SetFinalizer(i, checkRangeKeyFragmentBlockIterator) + return i + }, +} + +func checkSingleLevelIterator(obj interface{}) { + i := obj.(*singleLevelIterator) + if p := i.data.handle.Get(); p != nil { + fmt.Fprintf(os.Stderr, "singleLevelIterator.data.handle is not nil: %p\n", p) + os.Exit(1) + } + if p := i.index.handle.Get(); p != nil { + fmt.Fprintf(os.Stderr, "singleLevelIterator.index.handle is not nil: %p\n", p) + os.Exit(1) + } +} + +func checkTwoLevelIterator(obj interface{}) { + i := obj.(*twoLevelIterator) + if p := i.data.handle.Get(); p != nil { + fmt.Fprintf(os.Stderr, "singleLevelIterator.data.handle is not nil: %p\n", p) + os.Exit(1) + } + if p := i.index.handle.Get(); p != nil { + fmt.Fprintf(os.Stderr, "singleLevelIterator.index.handle is not nil: %p\n", p) + os.Exit(1) + } +} + +func checkRangeKeyFragmentBlockIterator(obj interface{}) { + i := obj.(*rangeKeyFragmentBlockIter) + if p := i.blockIter.handle.Get(); p != nil { + fmt.Fprintf(os.Stderr, "fragmentBlockIter.blockIter.handle is not nil: %p\n", p) + os.Exit(1) + } +} + +// compactionIterator is similar to Iterator but it increments the number of +// bytes that have been iterated through. +type compactionIterator struct { + *singleLevelIterator + bytesIterated *uint64 + prevOffset uint64 +} + +// compactionIterator implements the base.InternalIterator interface. +var _ base.InternalIterator = (*compactionIterator)(nil) + +func (i *compactionIterator) String() string { + if i.vState != nil { + return i.vState.fileNum.String() + } + return i.reader.fileNum.String() +} + +func (i *compactionIterator) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + panic("pebble: SeekGE unimplemented") +} + +func (i *compactionIterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("pebble: SeekPrefixGE unimplemented") +} + +func (i *compactionIterator) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*InternalKey, base.LazyValue) { + panic("pebble: SeekLT unimplemented") +} + +func (i *compactionIterator) First() (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + return i.skipForward(i.singleLevelIterator.First()) +} + +func (i *compactionIterator) Last() (*InternalKey, base.LazyValue) { + panic("pebble: Last unimplemented") +} + +// Note: compactionIterator.Next mirrors the implementation of Iterator.Next +// due to performance. Keep the two in sync. +func (i *compactionIterator) Next() (*InternalKey, base.LazyValue) { + if i.err != nil { + return nil, base.LazyValue{} + } + return i.skipForward(i.data.Next()) +} + +func (i *compactionIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { + panic("pebble: NextPrefix unimplemented") +} + +func (i *compactionIterator) Prev() (*InternalKey, base.LazyValue) { + panic("pebble: Prev unimplemented") +} + +func (i *compactionIterator) skipForward( + key *InternalKey, val base.LazyValue, +) (*InternalKey, base.LazyValue) { + if key == nil { + for { + if key, _ := i.index.Next(); key == nil { + break + } + result := i.loadBlock(+1) + if result != loadBlockOK { + if i.err != nil { + break + } + switch result { + case loadBlockFailed: + // We checked that i.index was at a valid entry, so + // loadBlockFailed could not have happened due to to i.index + // being exhausted, and must be due to an error. + panic("loadBlock should not have failed with no error") + case loadBlockIrrelevant: + panic("compactionIter should not be using block intervals for skipping") + default: + panic(fmt.Sprintf("unexpected case %d", result)) + } + } + // result == loadBlockOK + if key, val = i.data.First(); key != nil { + break + } + } + } + + curOffset := i.recordOffset() + *i.bytesIterated += uint64(curOffset - i.prevOffset) + i.prevOffset = curOffset + + if i.vState != nil && key != nil { + cmp := i.cmp(key.UserKey, i.vState.upper.UserKey) + if cmp > 0 || (i.vState.upper.IsExclusiveSentinel() && cmp == 0) { + return nil, base.LazyValue{} + } + } + + return key, val +} diff --git a/vendor/github.com/cockroachdb/pebble/sstable/reader_iter_single_lvl.go b/vendor/github.com/cockroachdb/pebble/sstable/reader_iter_single_lvl.go new file mode 100644 index 0000000..8b094cd --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/sstable/reader_iter_single_lvl.go @@ -0,0 +1,1391 @@ +// Copyright 2011 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 sstable + +import ( + "context" + "fmt" + "unsafe" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" +) + +// singleLevelIterator iterates over an entire table of data. To seek for a given +// key, it first looks in the index for the block that contains that key, and then +// looks inside that block. +type singleLevelIterator struct { + ctx context.Context + cmp Compare + // Global lower/upper bound for the iterator. + lower []byte + upper []byte + bpfs *BlockPropertiesFilterer + // Per-block lower/upper bound. Nil if the bound does not apply to the block + // because we determined the block lies completely within the bound. + blockLower []byte + blockUpper []byte + reader *Reader + // vState will be set iff the iterator is constructed for virtual sstable + // iteration. + vState *virtualState + // endKeyInclusive is set to force the iterator to treat the upper field as + // inclusive while iterating instead of exclusive. + endKeyInclusive bool + index blockIter + data blockIter + dataRH objstorage.ReadHandle + dataRHPrealloc objstorageprovider.PreallocatedReadHandle + // dataBH refers to the last data block that the iterator considered + // loading. It may not actually have loaded the block, due to an error or + // because it was considered irrelevant. + dataBH BlockHandle + vbReader *valueBlockReader + // vbRH is the read handle for value blocks, which are in a different + // part of the sstable than data blocks. + vbRH objstorage.ReadHandle + vbRHPrealloc objstorageprovider.PreallocatedReadHandle + err error + closeHook func(i Iterator) error + stats *base.InternalIteratorStats + bufferPool *BufferPool + + // boundsCmp and positionedUsingLatestBounds are for optimizing iteration + // that uses multiple adjacent bounds. The seek after setting a new bound + // can use the fact that the iterator is either within the previous bounds + // or exactly one key before or after the bounds. If the new bounds is + // after/before the previous bounds, and we are already positioned at a + // block that is relevant for the new bounds, we can try to first position + // using Next/Prev (repeatedly) instead of doing a more expensive seek. + // + // When there are wide files at higher levels that match the bounds + // but don't have any data for the bound, we will already be + // positioned at the key beyond the bounds and won't need to do much + // work -- given that most data is in L6, such files are likely to + // dominate the performance of the mergingIter, and may be the main + // benefit of this performance optimization (of course it also helps + // when the file that has the data has successive seeks that stay in + // the same block). + // + // Specifically, boundsCmp captures the relationship between the previous + // and current bounds, if the iterator had been positioned after setting + // the previous bounds. If it was not positioned, i.e., Seek/First/Last + // were not called, we don't know where it is positioned and cannot + // optimize. + // + // Example: Bounds moving forward, and iterator exhausted in forward direction. + // bounds = [f, h), ^ shows block iterator position + // file contents [ a b c d e f g h i j k ] + // ^ + // new bounds = [j, k). Since positionedUsingLatestBounds=true, boundsCmp is + // set to +1. SeekGE(j) can use next (the optimization also requires that j + // is within the block, but that is not for correctness, but to limit the + // optimization to when it will actually be an optimization). + // + // Example: Bounds moving forward. + // bounds = [f, h), ^ shows block iterator position + // file contents [ a b c d e f g h i j k ] + // ^ + // new bounds = [j, k). Since positionedUsingLatestBounds=true, boundsCmp is + // set to +1. SeekGE(j) can use next. + // + // Example: Bounds moving forward, but iterator not positioned using previous + // bounds. + // bounds = [f, h), ^ shows block iterator position + // file contents [ a b c d e f g h i j k ] + // ^ + // new bounds = [i, j). Iterator is at j since it was never positioned using + // [f, h). So positionedUsingLatestBounds=false, and boundsCmp is set to 0. + // SeekGE(i) will not use next. + // + // Example: Bounds moving forward and sparse file + // bounds = [f, h), ^ shows block iterator position + // file contents [ a z ] + // ^ + // new bounds = [j, k). Since positionedUsingLatestBounds=true, boundsCmp is + // set to +1. SeekGE(j) notices that the iterator is already past j and does + // not need to do anything. + // + // Similar examples can be constructed for backward iteration. + // + // This notion of exactly one key before or after the bounds is not quite + // true when block properties are used to ignore blocks. In that case we + // can't stop precisely at the first block that is past the bounds since + // we are using the index entries to enforce the bounds. + // + // e.g. 3 blocks with keys [b, c] [f, g], [i, j, k] with index entries d, + // h, l. And let the lower bound be k, and we are reverse iterating. If + // the block [i, j, k] is ignored due to the block interval annotations we + // do need to move the index to block [f, g] since the index entry for the + // [i, j, k] block is l which is not less than the lower bound of k. So we + // have passed the entries i, j. + // + // This behavior is harmless since the block property filters are fixed + // for the lifetime of the iterator so i, j are irrelevant. In addition, + // the current code will not load the [f, g] block, so the seek + // optimization that attempts to use Next/Prev do not apply anyway. + boundsCmp int + positionedUsingLatestBounds bool + + // exhaustedBounds represents whether the iterator is exhausted for + // iteration by reaching the upper or lower bound. +1 when exhausted + // the upper bound, -1 when exhausted the lower bound, and 0 when + // neither. exhaustedBounds is also used for the TrySeekUsingNext + // optimization in twoLevelIterator and singleLevelIterator. Care should be + // taken in setting this in twoLevelIterator before calling into + // singleLevelIterator, given that these two iterators share this field. + exhaustedBounds int8 + + // maybeFilteredKeysSingleLevel indicates whether the last iterator + // positioning operation may have skipped any data blocks due to + // block-property filters when positioning the index. + maybeFilteredKeysSingleLevel bool + + // useFilter specifies whether the filter block in this sstable, if present, + // should be used for prefix seeks or not. In some cases it is beneficial + // to skip a filter block even if it exists (eg. if probability of a match + // is high). + useFilter bool + lastBloomFilterMatched bool + + hideObsoletePoints bool +} + +// singleLevelIterator implements the base.InternalIterator interface. +var _ base.InternalIterator = (*singleLevelIterator)(nil) + +// init initializes a singleLevelIterator for reading from the table. It is +// synonmous with Reader.NewIter, but allows for reusing of the iterator +// between different Readers. +// +// Note that lower, upper passed into init has nothing to do with virtual sstable +// bounds. If the virtualState passed in is not nil, then virtual sstable bounds +// will be enforced. +func (i *singleLevelIterator) init( + ctx context.Context, + r *Reader, + v *virtualState, + lower, upper []byte, + filterer *BlockPropertiesFilterer, + useFilter, hideObsoletePoints bool, + stats *base.InternalIteratorStats, + rp ReaderProvider, + bufferPool *BufferPool, +) error { + if r.err != nil { + return r.err + } + indexH, err := r.readIndex(ctx, stats) + if err != nil { + return err + } + if v != nil { + i.vState = v + i.endKeyInclusive, lower, upper = v.constrainBounds(lower, upper, false /* endInclusive */) + } + + i.ctx = ctx + i.lower = lower + i.upper = upper + i.bpfs = filterer + i.useFilter = useFilter + i.reader = r + i.cmp = r.Compare + i.stats = stats + i.hideObsoletePoints = hideObsoletePoints + i.bufferPool = bufferPool + err = i.index.initHandle(i.cmp, indexH, r.Properties.GlobalSeqNum, false) + if err != nil { + // blockIter.Close releases indexH and always returns a nil error + _ = i.index.Close() + return err + } + i.dataRH = objstorageprovider.UsePreallocatedReadHandle(ctx, r.readable, &i.dataRHPrealloc) + if r.tableFormat >= TableFormatPebblev3 { + if r.Properties.NumValueBlocks > 0 { + // NB: we cannot avoid this ~248 byte allocation, since valueBlockReader + // can outlive the singleLevelIterator due to be being embedded in a + // LazyValue. This consumes ~2% in microbenchmark CPU profiles, but we + // should only optimize this if it shows up as significant in end-to-end + // CockroachDB benchmarks, since it is tricky to do so. One possibility + // is that if many sstable iterators only get positioned at latest + // versions of keys, and therefore never expose a LazyValue that is + // separated to their callers, they can put this valueBlockReader into a + // sync.Pool. + i.vbReader = &valueBlockReader{ + ctx: ctx, + bpOpen: i, + rp: rp, + vbih: r.valueBIH, + stats: stats, + } + i.data.lazyValueHandling.vbr = i.vbReader + i.vbRH = objstorageprovider.UsePreallocatedReadHandle(ctx, r.readable, &i.vbRHPrealloc) + } + i.data.lazyValueHandling.hasValuePrefix = true + } + return nil +} + +// Helper function to check if keys returned from iterator are within global and virtual bounds. +func (i *singleLevelIterator) maybeVerifyKey( + iKey *InternalKey, val base.LazyValue, +) (*InternalKey, base.LazyValue) { + // maybeVerify key is only used for virtual sstable iterators. + if invariants.Enabled && i.vState != nil && iKey != nil { + key := iKey.UserKey + + uc, vuc := i.cmp(key, i.upper), i.cmp(key, i.vState.upper.UserKey) + lc, vlc := i.cmp(key, i.lower), i.cmp(key, i.vState.lower.UserKey) + + if (i.vState.upper.IsExclusiveSentinel() && vuc == 0) || (!i.endKeyInclusive && uc == 0) || uc > 0 || vuc > 0 || lc < 0 || vlc < 0 { + panic(fmt.Sprintf("key: %s out of bounds of singleLevelIterator", key)) + } + } + return iKey, val +} + +// setupForCompaction sets up the singleLevelIterator for use with compactionIter. +// Currently, it skips readahead ramp-up. It should be called after init is called. +func (i *singleLevelIterator) setupForCompaction() { + i.dataRH.SetupForCompaction() + if i.vbRH != nil { + i.vbRH.SetupForCompaction() + } +} + +func (i *singleLevelIterator) resetForReuse() singleLevelIterator { + return singleLevelIterator{ + index: i.index.resetForReuse(), + data: i.data.resetForReuse(), + } +} + +func (i *singleLevelIterator) initBounds() { + // Trim the iteration bounds for the current block. We don't have to check + // the bounds on each iteration if the block is entirely contained within the + // iteration bounds. + i.blockLower = i.lower + if i.blockLower != nil { + key, _ := i.data.First() + if key != nil && i.cmp(i.blockLower, key.UserKey) < 0 { + // The lower-bound is less than the first key in the block. No need + // to check the lower-bound again for this block. + i.blockLower = nil + } + } + i.blockUpper = i.upper + if i.blockUpper != nil && i.cmp(i.blockUpper, i.index.Key().UserKey) > 0 { + // The upper-bound is greater than the index key which itself is greater + // than or equal to every key in the block. No need to check the + // upper-bound again for this block. Even if blockUpper is inclusive + // because of upper being inclusive, we can still safely set blockUpper + // to nil here. + // + // TODO(bananabrick): We could also set blockUpper to nil for the >= + // case, if blockUpper is inclusive. + i.blockUpper = nil + } +} + +// Deterministic disabling of the bounds-based optimization that avoids seeking. +// Uses the iterator pointer, since we want diversity in iterator behavior for +// the same SetBounds call. Used for tests. +func disableBoundsOpt(bound []byte, ptr uintptr) bool { + // Fibonacci hash https://probablydance.com/2018/06/16/fibonacci-hashing-the-optimization-that-the-world-forgot-or-a-better-alternative-to-integer-modulo/ + simpleHash := (11400714819323198485 * uint64(ptr)) >> 63 + return bound[len(bound)-1]&byte(1) == 0 && simpleHash == 0 +} + +// ensureBoundsOptDeterminism provides a facility for disabling of the bounds +// optimizations performed by disableBoundsOpt for tests that require +// deterministic iterator behavior. Some unit tests examine internal iterator +// state and require this behavior to be deterministic. +var ensureBoundsOptDeterminism bool + +// SetBounds implements internalIterator.SetBounds, as documented in the pebble +// package. Note that the upper field is exclusive. +func (i *singleLevelIterator) SetBounds(lower, upper []byte) { + i.boundsCmp = 0 + if i.vState != nil { + // If the reader is constructed for a virtual sstable, then we must + // constrain the bounds of the reader. For physical sstables, the bounds + // can be wider than the actual sstable's bounds because we won't + // accidentally expose additional keys as there are no additional keys. + i.endKeyInclusive, lower, upper = i.vState.constrainBounds( + lower, upper, false, + ) + } else { + // TODO(bananabrick): Figure out the logic here to enable the boundsCmp + // optimization for virtual sstables. + if i.positionedUsingLatestBounds { + if i.upper != nil && lower != nil && i.cmp(i.upper, lower) <= 0 { + i.boundsCmp = +1 + if invariants.Enabled && !ensureBoundsOptDeterminism && + disableBoundsOpt(lower, uintptr(unsafe.Pointer(i))) { + i.boundsCmp = 0 + } + } else if i.lower != nil && upper != nil && i.cmp(upper, i.lower) <= 0 { + i.boundsCmp = -1 + if invariants.Enabled && !ensureBoundsOptDeterminism && + disableBoundsOpt(upper, uintptr(unsafe.Pointer(i))) { + i.boundsCmp = 0 + } + } + } + } + + i.positionedUsingLatestBounds = false + i.lower = lower + i.upper = upper + i.blockLower = nil + i.blockUpper = nil +} + +// loadBlock loads the block at the current index position and leaves i.data +// unpositioned. If unsuccessful, it sets i.err to any error encountered, which +// may be nil if we have simply exhausted the entire table. +func (i *singleLevelIterator) loadBlock(dir int8) loadBlockResult { + if !i.index.valid() { + // Ensure the data block iterator is invalidated even if loading of the block + // fails. + i.data.invalidate() + return loadBlockFailed + } + // Load the next block. + v := i.index.value() + bhp, err := decodeBlockHandleWithProperties(v.InPlaceValue()) + if i.dataBH == bhp.BlockHandle && i.data.valid() { + // We're already at the data block we want to load. Reset bounds in case + // they changed since the last seek, but don't reload the block from cache + // or disk. + // + // It's safe to leave i.data in its original state here, as all callers to + // loadBlock make an absolute positioning call (i.e. a seek, first, or last) + // to `i.data` right after loadBlock returns loadBlockOK. + i.initBounds() + return loadBlockOK + } + // Ensure the data block iterator is invalidated even if loading of the block + // fails. + i.data.invalidate() + i.dataBH = bhp.BlockHandle + if err != nil { + i.err = errCorruptIndexEntry + return loadBlockFailed + } + if i.bpfs != nil { + intersects, err := i.bpfs.intersects(bhp.Props) + if err != nil { + i.err = errCorruptIndexEntry + return loadBlockFailed + } + if intersects == blockMaybeExcluded { + intersects = i.resolveMaybeExcluded(dir) + } + if intersects == blockExcluded { + i.maybeFilteredKeysSingleLevel = true + return loadBlockIrrelevant + } + // blockIntersects + } + ctx := objiotracing.WithBlockType(i.ctx, objiotracing.DataBlock) + block, err := i.reader.readBlock(ctx, i.dataBH, nil /* transform */, i.dataRH, i.stats, i.bufferPool) + if err != nil { + i.err = err + return loadBlockFailed + } + i.err = i.data.initHandle(i.cmp, block, i.reader.Properties.GlobalSeqNum, i.hideObsoletePoints) + if i.err != nil { + // The block is partially loaded, and we don't want it to appear valid. + i.data.invalidate() + return loadBlockFailed + } + i.initBounds() + return loadBlockOK +} + +// readBlockForVBR implements the blockProviderWhenOpen interface for use by +// the valueBlockReader. +func (i *singleLevelIterator) readBlockForVBR( + ctx context.Context, h BlockHandle, stats *base.InternalIteratorStats, +) (bufferHandle, error) { + ctx = objiotracing.WithBlockType(ctx, objiotracing.ValueBlock) + return i.reader.readBlock(ctx, h, nil, i.vbRH, stats, i.bufferPool) +} + +// resolveMaybeExcluded is invoked when the block-property filterer has found +// that a block is excluded according to its properties but only if its bounds +// fall within the filter's current bounds. This function consults the +// apprioriate bound, depending on the iteration direction, and returns either +// `blockIntersects` or `blockMaybeExcluded`. +func (i *singleLevelIterator) resolveMaybeExcluded(dir int8) intersectsResult { + // TODO(jackson): We could first try comparing to top-level index block's + // key, and if within bounds avoid per-data block key comparisons. + + // This iterator is configured with a bound-limited block property + // filter. The bpf determined this block could be excluded from + // iteration based on the property encoded in the block handle. + // However, we still need to determine if the block is wholly + // contained within the filter's key bounds. + // + // External guarantees ensure all the block's keys are ≥ the + // filter's lower bound during forward iteration, and that all the + // block's keys are < the filter's upper bound during backward + // iteration. We only need to determine if the opposite bound is + // also met. + // + // The index separator in index.Key() provides an inclusive + // upper-bound for the data block's keys, guaranteeing that all its + // keys are ≤ index.Key(). For forward iteration, this is all we + // need. + if dir > 0 { + // Forward iteration. + if i.bpfs.boundLimitedFilter.KeyIsWithinUpperBound(i.index.Key().UserKey) { + return blockExcluded + } + return blockIntersects + } + + // Reverse iteration. + // + // Because we're iterating in the reverse direction, we don't yet have + // enough context available to determine if the block is wholly contained + // within its bounds. This case arises only during backward iteration, + // because of the way the index is structured. + // + // Consider a bound-limited bpf limited to the bounds [b,d), loading the + // block with separator `c`. During reverse iteration, the guarantee that + // all the block's keys are < `d` is externally provided, but no guarantee + // is made on the bpf's lower bound. The separator `c` only provides an + // inclusive upper bound on the block's keys, indicating that the + // corresponding block handle points to a block containing only keys ≤ `c`. + // + // To establish a lower bound, we step the index backwards to read the + // previous block's separator, which provides an inclusive lower bound on + // the original block's keys. Afterwards, we step forward to restore our + // index position. + if peekKey, _ := i.index.Prev(); peekKey == nil { + // The original block points to the first block of this index block. If + // there's a two-level index, it could potentially provide a lower + // bound, but the code refactoring necessary to read it doesn't seem + // worth the payoff. We fall through to loading the block. + } else if i.bpfs.boundLimitedFilter.KeyIsWithinLowerBound(peekKey.UserKey) { + // The lower-bound on the original block falls within the filter's + // bounds, and we can skip the block (after restoring our current index + // position). + _, _ = i.index.Next() + return blockExcluded + } + _, _ = i.index.Next() + return blockIntersects +} + +func (i *singleLevelIterator) initBoundsForAlreadyLoadedBlock() { + if i.data.getFirstUserKey() == nil { + panic("initBoundsForAlreadyLoadedBlock must not be called on empty or corrupted block") + } + i.blockLower = i.lower + if i.blockLower != nil { + firstUserKey := i.data.getFirstUserKey() + if firstUserKey != nil && i.cmp(i.blockLower, firstUserKey) < 0 { + // The lower-bound is less than the first key in the block. No need + // to check the lower-bound again for this block. + i.blockLower = nil + } + } + i.blockUpper = i.upper + if i.blockUpper != nil && i.cmp(i.blockUpper, i.index.Key().UserKey) > 0 { + // The upper-bound is greater than the index key which itself is greater + // than or equal to every key in the block. No need to check the + // upper-bound again for this block. + i.blockUpper = nil + } +} + +// The number of times to call Next/Prev in a block before giving up and seeking. +// The value of 4 is arbitrary. +// TODO(sumeer): experiment with dynamic adjustment based on the history of +// seeks for a particular iterator. +const numStepsBeforeSeek = 4 + +func (i *singleLevelIterator) trySeekGEUsingNextWithinBlock( + key []byte, +) (k *InternalKey, v base.LazyValue, done bool) { + k, v = i.data.Key(), i.data.value() + for j := 0; j < numStepsBeforeSeek; j++ { + curKeyCmp := i.cmp(k.UserKey, key) + if curKeyCmp >= 0 { + if i.blockUpper != nil { + cmp := i.cmp(k.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{}, true + } + } + return k, v, true + } + k, v = i.data.Next() + if k == nil { + break + } + } + return k, v, false +} + +func (i *singleLevelIterator) trySeekLTUsingPrevWithinBlock( + key []byte, +) (k *InternalKey, v base.LazyValue, done bool) { + k, v = i.data.Key(), i.data.value() + for j := 0; j < numStepsBeforeSeek; j++ { + curKeyCmp := i.cmp(k.UserKey, key) + if curKeyCmp < 0 { + if i.blockLower != nil && i.cmp(k.UserKey, i.blockLower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{}, true + } + return k, v, true + } + k, v = i.data.Prev() + if k == nil { + break + } + } + return k, v, false +} + +func (i *singleLevelIterator) recordOffset() uint64 { + offset := i.dataBH.Offset + if i.data.valid() { + // - i.dataBH.Length/len(i.data.data) is the compression ratio. If + // uncompressed, this is 1. + // - i.data.nextOffset is the uncompressed position of the current record + // in the block. + // - i.dataBH.Offset is the offset of the block in the sstable before + // decompression. + offset += (uint64(i.data.nextOffset) * i.dataBH.Length) / uint64(len(i.data.data)) + } else { + // Last entry in the block must increment bytes iterated by the size of the block trailer + // and restart points. + offset += i.dataBH.Length + blockTrailerLen + } + return offset +} + +// SeekGE implements internalIterator.SeekGE, as documented in the pebble +// package. Note that SeekGE only checks the upper bound. It is up to the +// caller to ensure that key is greater than or equal to the lower bound. +func (i *singleLevelIterator) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + if i.vState != nil { + // Callers of SeekGE don't know about virtual sstable bounds, so we may + // have to internally restrict the bounds. + // + // TODO(bananabrick): We can optimize this check away for the level iter + // if necessary. + if i.cmp(key, i.lower) < 0 { + key = i.lower + } + } + + if flags.TrySeekUsingNext() { + // The i.exhaustedBounds comparison indicates that the upper bound was + // reached. The i.data.isDataInvalidated() indicates that the sstable was + // exhausted. + if (i.exhaustedBounds == +1 || i.data.isDataInvalidated()) && i.err == nil { + // Already exhausted, so return nil. + return nil, base.LazyValue{} + } + if i.err != nil { + // The current iterator position cannot be used. + flags = flags.DisableTrySeekUsingNext() + } + // INVARIANT: flags.TrySeekUsingNext() => i.err == nil && + // !i.exhaustedBounds==+1 && !i.data.isDataInvalidated(). That is, + // data-exhausted and bounds-exhausted, as defined earlier, are both + // false. Ths makes it safe to clear out i.exhaustedBounds and i.err + // before calling into seekGEHelper. + } + + i.exhaustedBounds = 0 + i.err = nil // clear cached iteration error + boundsCmp := i.boundsCmp + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + i.positionedUsingLatestBounds = true + return i.seekGEHelper(key, boundsCmp, flags) +} + +// seekGEHelper contains the common functionality for SeekGE and SeekPrefixGE. +func (i *singleLevelIterator) seekGEHelper( + key []byte, boundsCmp int, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + // Invariant: trySeekUsingNext => !i.data.isDataInvalidated() && i.exhaustedBounds != +1 + + // SeekGE performs various step-instead-of-seeking optimizations: eg enabled + // by trySeekUsingNext, or by monotonically increasing bounds (i.boundsCmp). + // Care must be taken to ensure that when performing these optimizations and + // the iterator becomes exhausted, i.maybeFilteredKeys is set appropriately. + // Consider a previous SeekGE that filtered keys from k until the current + // iterator position. + // + // If the previous SeekGE exhausted the iterator, it's possible keys greater + // than or equal to the current search key were filtered. We must not reuse + // the current iterator position without remembering the previous value of + // maybeFilteredKeys. + + var dontSeekWithinBlock bool + if !i.data.isDataInvalidated() && !i.index.isDataInvalidated() && i.data.valid() && i.index.valid() && + boundsCmp > 0 && i.cmp(key, i.index.Key().UserKey) <= 0 { + // Fast-path: The bounds have moved forward and this SeekGE is + // respecting the lower bound (guaranteed by Iterator). We know that + // the iterator must already be positioned within or just outside the + // previous bounds. Therefore it cannot be positioned at a block (or + // the position within that block) that is ahead of the seek position. + // However it can be positioned at an earlier block. This fast-path to + // use Next() on the block is only applied when we are already at the + // block that the slow-path (the else-clause) would load -- this is + // the motivation for the i.cmp(key, i.index.Key().UserKey) <= 0 + // predicate. + i.initBoundsForAlreadyLoadedBlock() + ikey, val, done := i.trySeekGEUsingNextWithinBlock(key) + if done { + return ikey, val + } + if ikey == nil { + // Done with this block. + dontSeekWithinBlock = true + } + } else { + // Cannot use bounds monotonicity. But may be able to optimize if + // caller claimed externally known invariant represented by + // flags.TrySeekUsingNext(). + if flags.TrySeekUsingNext() { + // seekPrefixGE or SeekGE has already ensured + // !i.data.isDataInvalidated() && i.exhaustedBounds != +1 + currKey := i.data.Key() + value := i.data.value() + less := i.cmp(currKey.UserKey, key) < 0 + // We could be more sophisticated and confirm that the seek + // position is within the current block before applying this + // optimization. But there may be some benefit even if it is in + // the next block, since we can avoid seeking i.index. + for j := 0; less && j < numStepsBeforeSeek; j++ { + currKey, value = i.Next() + if currKey == nil { + return nil, base.LazyValue{} + } + less = i.cmp(currKey.UserKey, key) < 0 + } + if !less { + if i.blockUpper != nil { + cmp := i.cmp(currKey.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + return currKey, value + } + } + + // Slow-path. + // Since we're re-seeking the iterator, the previous value of + // maybeFilteredKeysSingleLevel is irrelevant. If we filter out blocks + // during seeking, loadBlock will set it to true. + i.maybeFilteredKeysSingleLevel = false + + var ikey *InternalKey + if ikey, _ = i.index.SeekGE(key, flags.DisableTrySeekUsingNext()); ikey == nil { + // The target key is greater than any key in the index block. + // Invalidate the block iterator so that a subsequent call to Prev() + // will return the last key in the table. + i.data.invalidate() + return nil, base.LazyValue{} + } + result := i.loadBlock(+1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockIrrelevant { + // Enforce the upper bound here since don't want to bother moving + // to the next block if upper bound is already exceeded. Note that + // the next block starts with keys >= ikey.UserKey since even + // though this is the block separator, the same user key can span + // multiple blocks. If upper is exclusive we use >= below, else + // we use >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + // Want to skip to the next block. + dontSeekWithinBlock = true + } + } + if !dontSeekWithinBlock { + if ikey, val := i.data.SeekGE(key, flags.DisableTrySeekUsingNext()); ikey != nil { + if i.blockUpper != nil { + cmp := i.cmp(ikey.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + return ikey, val + } + } + return i.skipForward() +} + +// SeekPrefixGE implements internalIterator.SeekPrefixGE, as documented in the +// pebble package. Note that SeekPrefixGE only checks the upper bound. It is up +// to the caller to ensure that key is greater than or equal to the lower bound. +func (i *singleLevelIterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + if i.vState != nil { + // Callers of SeekPrefixGE aren't aware of virtual sstable bounds, so + // we may have to internally restrict the bounds. + // + // TODO(bananabrick): We can optimize away this check for the level iter + // if necessary. + if i.cmp(key, i.lower) < 0 { + key = i.lower + } + } + return i.seekPrefixGE(prefix, key, flags, i.useFilter) +} + +func (i *singleLevelIterator) seekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, checkFilter bool, +) (k *InternalKey, value base.LazyValue) { + // NOTE: prefix is only used for bloom filter checking and not later work in + // this method. Hence, we can use the existing iterator position if the last + // SeekPrefixGE did not fail bloom filter matching. + + err := i.err + i.err = nil // clear cached iteration error + if checkFilter && i.reader.tableFilter != nil { + if !i.lastBloomFilterMatched { + // Iterator is not positioned based on last seek. + flags = flags.DisableTrySeekUsingNext() + } + i.lastBloomFilterMatched = false + // Check prefix bloom filter. + var dataH bufferHandle + dataH, i.err = i.reader.readFilter(i.ctx, i.stats) + if i.err != nil { + i.data.invalidate() + return nil, base.LazyValue{} + } + mayContain := i.reader.tableFilter.mayContain(dataH.Get(), prefix) + dataH.Release() + if !mayContain { + // This invalidation may not be necessary for correctness, and may + // be a place to optimize later by reusing the already loaded + // block. It was necessary in earlier versions of the code since + // the caller was allowed to call Next when SeekPrefixGE returned + // nil. This is no longer allowed. + i.data.invalidate() + return nil, base.LazyValue{} + } + i.lastBloomFilterMatched = true + } + if flags.TrySeekUsingNext() { + // The i.exhaustedBounds comparison indicates that the upper bound was + // reached. The i.data.isDataInvalidated() indicates that the sstable was + // exhausted. + if (i.exhaustedBounds == +1 || i.data.isDataInvalidated()) && err == nil { + // Already exhausted, so return nil. + return nil, base.LazyValue{} + } + if err != nil { + // The current iterator position cannot be used. + flags = flags.DisableTrySeekUsingNext() + } + // INVARIANT: flags.TrySeekUsingNext() => err == nil && + // !i.exhaustedBounds==+1 && !i.data.isDataInvalidated(). That is, + // data-exhausted and bounds-exhausted, as defined earlier, are both + // false. Ths makes it safe to clear out i.exhaustedBounds and i.err + // before calling into seekGEHelper. + } + // Bloom filter matches, or skipped, so this method will position the + // iterator. + i.exhaustedBounds = 0 + boundsCmp := i.boundsCmp + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + i.positionedUsingLatestBounds = true + k, value = i.seekGEHelper(key, boundsCmp, flags) + return i.maybeVerifyKey(k, value) +} + +// virtualLast should only be called if i.vReader != nil. +func (i *singleLevelIterator) virtualLast() (*InternalKey, base.LazyValue) { + if i.vState == nil { + panic("pebble: invalid call to virtualLast") + } + + // Seek to the first internal key. + ikey, _ := i.SeekGE(i.upper, base.SeekGEFlagsNone) + if i.endKeyInclusive { + // Let's say the virtual sstable upper bound is c#1, with the keys c#3, c#2, + // c#1, d, e, ... in the sstable. So, the last key in the virtual sstable is + // c#1. We can perform SeekGE(i.upper) and then keep nexting until we find + // the last key with userkey == i.upper. + // + // TODO(bananabrick): Think about how to improve this. If many internal keys + // with the same user key at the upper bound then this could be slow, but + // maybe the odds of having many internal keys with the same user key at the + // upper bound are low. + for ikey != nil && i.cmp(ikey.UserKey, i.upper) == 0 { + ikey, _ = i.Next() + } + return i.Prev() + } + + // We seeked to the first key >= i.upper. + return i.Prev() +} + +// SeekLT implements internalIterator.SeekLT, as documented in the pebble +// package. Note that SeekLT only checks the lower bound. It is up to the +// caller to ensure that key is less than or equal to the upper bound. +func (i *singleLevelIterator) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*InternalKey, base.LazyValue) { + if i.vState != nil { + // Might have to fix upper bound since virtual sstable bounds are not + // known to callers of SeekLT. + // + // TODO(bananabrick): We can optimize away this check for the level iter + // if necessary. + cmp := i.cmp(key, i.upper) + // key == i.upper is fine. We'll do the right thing and return the + // first internal key with user key < key. + if cmp > 0 { + // Return the last key in the virtual sstable. + return i.virtualLast() + } + } + + i.exhaustedBounds = 0 + i.err = nil // clear cached iteration error + boundsCmp := i.boundsCmp + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + // Seeking operations perform various step-instead-of-seeking optimizations: + // eg by considering monotonically increasing bounds (i.boundsCmp). Care + // must be taken to ensure that when performing these optimizations and the + // iterator becomes exhausted i.maybeFilteredKeysSingleLevel is set + // appropriately. Consider a previous SeekLT that filtered keys from k + // until the current iterator position. + // + // If the previous SeekLT did exhausted the iterator, it's possible keys + // less than the current search key were filtered. We must not reuse the + // current iterator position without remembering the previous value of + // maybeFilteredKeysSingleLevel. + + i.positionedUsingLatestBounds = true + + var dontSeekWithinBlock bool + if !i.data.isDataInvalidated() && !i.index.isDataInvalidated() && i.data.valid() && i.index.valid() && + boundsCmp < 0 && i.cmp(i.data.getFirstUserKey(), key) < 0 { + // Fast-path: The bounds have moved backward, and this SeekLT is + // respecting the upper bound (guaranteed by Iterator). We know that + // the iterator must already be positioned within or just outside the + // previous bounds. Therefore it cannot be positioned at a block (or + // the position within that block) that is behind the seek position. + // However it can be positioned at a later block. This fast-path to + // use Prev() on the block is only applied when we are already at the + // block that can satisfy this seek -- this is the motivation for the + // the i.cmp(i.data.firstKey.UserKey, key) < 0 predicate. + i.initBoundsForAlreadyLoadedBlock() + ikey, val, done := i.trySeekLTUsingPrevWithinBlock(key) + if done { + return ikey, val + } + if ikey == nil { + // Done with this block. + dontSeekWithinBlock = true + } + } else { + // Slow-path. + i.maybeFilteredKeysSingleLevel = false + var ikey *InternalKey + + // NB: If a bound-limited block property filter is configured, it's + // externally ensured that the filter is disabled (through returning + // Intersects=false irrespective of the block props provided) during + // seeks. + if ikey, _ = i.index.SeekGE(key, base.SeekGEFlagsNone); ikey == nil { + ikey, _ = i.index.Last() + if ikey == nil { + return nil, base.LazyValue{} + } + } + // INVARIANT: ikey != nil. + result := i.loadBlock(-1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockIrrelevant { + // Enforce the lower bound here since don't want to bother moving + // to the previous block if lower bound is already exceeded. Note + // that the previous block starts with keys <= ikey.UserKey since + // even though this is the current block's separator, the same + // user key can span multiple blocks. + if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{} + } + // Want to skip to the previous block. + dontSeekWithinBlock = true + } + } + if !dontSeekWithinBlock { + if ikey, val := i.data.SeekLT(key, flags); ikey != nil { + if i.blockLower != nil && i.cmp(ikey.UserKey, i.blockLower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{} + } + return ikey, val + } + } + // The index contains separator keys which may lie between + // user-keys. Consider the user-keys: + // + // complete + // ---- new block --- + // complexion + // + // If these two keys end one block and start the next, the index key may + // be chosen as "compleu". The SeekGE in the index block will then point + // us to the block containing "complexion". If this happens, we want the + // last key from the previous data block. + return i.maybeVerifyKey(i.skipBackward()) +} + +// First implements internalIterator.First, as documented in the pebble +// package. Note that First only checks the upper bound. It is up to the caller +// to ensure that key is greater than or equal to the lower bound (e.g. via a +// call to SeekGE(lower)). +func (i *singleLevelIterator) First() (*InternalKey, base.LazyValue) { + // If the iterator was created on a virtual sstable, we will SeekGE to the + // lower bound instead of using First, because First does not respect + // bounds. + if i.vState != nil { + return i.SeekGE(i.lower, base.SeekGEFlagsNone) + } + + if i.lower != nil { + panic("singleLevelIterator.First() used despite lower bound") + } + i.positionedUsingLatestBounds = true + i.maybeFilteredKeysSingleLevel = false + + return i.firstInternal() +} + +// firstInternal is a helper used for absolute positioning in a single-level +// index file, or for positioning in the second-level index in a two-level +// index file. For the latter, one cannot make any claims about absolute +// positioning. +func (i *singleLevelIterator) firstInternal() (*InternalKey, base.LazyValue) { + i.exhaustedBounds = 0 + i.err = nil // clear cached iteration error + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + var ikey *InternalKey + if ikey, _ = i.index.First(); ikey == nil { + i.data.invalidate() + return nil, base.LazyValue{} + } + result := i.loadBlock(+1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.data.First(); ikey != nil { + if i.blockUpper != nil { + cmp := i.cmp(ikey.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + return ikey, val + } + // Else fall through to skipForward. + } else { + // result == loadBlockIrrelevant. Enforce the upper bound here since + // don't want to bother moving to the next block if upper bound is + // already exceeded. Note that the next block starts with keys >= + // ikey.UserKey since even though this is the block separator, the + // same user key can span multiple blocks. If upper is exclusive we + // use >= below, else we use >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + // Else fall through to skipForward. + } + + return i.skipForward() +} + +// Last implements internalIterator.Last, as documented in the pebble +// package. Note that Last only checks the lower bound. It is up to the caller +// to ensure that key is less than the upper bound (e.g. via a call to +// SeekLT(upper)) +func (i *singleLevelIterator) Last() (*InternalKey, base.LazyValue) { + if i.vState != nil { + return i.virtualLast() + } + + if i.upper != nil { + panic("singleLevelIterator.Last() used despite upper bound") + } + i.positionedUsingLatestBounds = true + i.maybeFilteredKeysSingleLevel = false + return i.lastInternal() +} + +// lastInternal is a helper used for absolute positioning in a single-level +// index file, or for positioning in the second-level index in a two-level +// index file. For the latter, one cannot make any claims about absolute +// positioning. +func (i *singleLevelIterator) lastInternal() (*InternalKey, base.LazyValue) { + i.exhaustedBounds = 0 + i.err = nil // clear cached iteration error + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + var ikey *InternalKey + if ikey, _ = i.index.Last(); ikey == nil { + i.data.invalidate() + return nil, base.LazyValue{} + } + result := i.loadBlock(-1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.data.Last(); ikey != nil { + if i.blockLower != nil && i.cmp(ikey.UserKey, i.blockLower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{} + } + return ikey, val + } + // Else fall through to skipBackward. + } else { + // result == loadBlockIrrelevant. Enforce the lower bound here since + // don't want to bother moving to the previous block if lower bound is + // already exceeded. Note that the previous block starts with keys <= + // key.UserKey since even though this is the current block's + // separator, the same user key can span multiple blocks. + if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{} + } + } + + return i.skipBackward() +} + +// Next implements internalIterator.Next, as documented in the pebble +// package. +// Note: compactionIterator.Next mirrors the implementation of Iterator.Next +// due to performance. Keep the two in sync. +func (i *singleLevelIterator) Next() (*InternalKey, base.LazyValue) { + if i.exhaustedBounds == +1 { + panic("Next called even though exhausted upper bound") + } + i.exhaustedBounds = 0 + i.maybeFilteredKeysSingleLevel = false + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + if i.err != nil { + return nil, base.LazyValue{} + } + if key, val := i.data.Next(); key != nil { + if i.blockUpper != nil { + cmp := i.cmp(key.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + return key, val + } + return i.skipForward() +} + +// NextPrefix implements (base.InternalIterator).NextPrefix. +func (i *singleLevelIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { + if i.exhaustedBounds == +1 { + panic("NextPrefix called even though exhausted upper bound") + } + i.exhaustedBounds = 0 + i.maybeFilteredKeysSingleLevel = false + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + if i.err != nil { + return nil, base.LazyValue{} + } + if key, val := i.data.NextPrefix(succKey); key != nil { + if i.blockUpper != nil { + cmp := i.cmp(key.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + return key, val + } + // Did not find prefix in the existing data block. This is the slow-path + // where we effectively seek the iterator. + var ikey *InternalKey + // The key is likely to be in the next data block, so try one step. + if ikey, _ = i.index.Next(); ikey == nil { + // The target key is greater than any key in the index block. + // Invalidate the block iterator so that a subsequent call to Prev() + // will return the last key in the table. + i.data.invalidate() + return nil, base.LazyValue{} + } + if i.cmp(succKey, ikey.UserKey) > 0 { + // Not in the next data block, so seek the index. + if ikey, _ = i.index.SeekGE(succKey, base.SeekGEFlagsNone); ikey == nil { + // The target key is greater than any key in the index block. + // Invalidate the block iterator so that a subsequent call to Prev() + // will return the last key in the table. + i.data.invalidate() + return nil, base.LazyValue{} + } + } + result := i.loadBlock(+1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockIrrelevant { + // Enforce the upper bound here since don't want to bother moving + // to the next block if upper bound is already exceeded. Note that + // the next block starts with keys >= ikey.UserKey since even + // though this is the block separator, the same user key can span + // multiple blocks. If upper is exclusive we use >= below, else we use + // >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + } else if key, val := i.data.SeekGE(succKey, base.SeekGEFlagsNone); key != nil { + if i.blockUpper != nil { + cmp := i.cmp(key.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + return i.maybeVerifyKey(key, val) + } + + return i.skipForward() +} + +// Prev implements internalIterator.Prev, as documented in the pebble +// package. +func (i *singleLevelIterator) Prev() (*InternalKey, base.LazyValue) { + if i.exhaustedBounds == -1 { + panic("Prev called even though exhausted lower bound") + } + i.exhaustedBounds = 0 + i.maybeFilteredKeysSingleLevel = false + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + if i.err != nil { + return nil, base.LazyValue{} + } + if key, val := i.data.Prev(); key != nil { + if i.blockLower != nil && i.cmp(key.UserKey, i.blockLower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{} + } + return key, val + } + return i.skipBackward() +} + +func (i *singleLevelIterator) skipForward() (*InternalKey, base.LazyValue) { + for { + var key *InternalKey + if key, _ = i.index.Next(); key == nil { + i.data.invalidate() + break + } + result := i.loadBlock(+1) + if result != loadBlockOK { + if i.err != nil { + break + } + if result == loadBlockFailed { + // We checked that i.index was at a valid entry, so + // loadBlockFailed could not have happened due to to i.index + // being exhausted, and must be due to an error. + panic("loadBlock should not have failed with no error") + } + // result == loadBlockIrrelevant. Enforce the upper bound here + // since don't want to bother moving to the next block if upper + // bound is already exceeded. Note that the next block starts with + // keys >= key.UserKey since even though this is the block + // separator, the same user key can span multiple blocks. If upper + // is exclusive we use >= below, else we use >. + if i.upper != nil { + cmp := i.cmp(key.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + continue + } + if key, val := i.data.First(); key != nil { + if i.blockUpper != nil { + cmp := i.cmp(key.UserKey, i.blockUpper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + return nil, base.LazyValue{} + } + } + return i.maybeVerifyKey(key, val) + } + } + return nil, base.LazyValue{} +} + +func (i *singleLevelIterator) skipBackward() (*InternalKey, base.LazyValue) { + for { + var key *InternalKey + if key, _ = i.index.Prev(); key == nil { + i.data.invalidate() + break + } + result := i.loadBlock(-1) + if result != loadBlockOK { + if i.err != nil { + break + } + if result == loadBlockFailed { + // We checked that i.index was at a valid entry, so + // loadBlockFailed could not have happened due to to i.index + // being exhausted, and must be due to an error. + panic("loadBlock should not have failed with no error") + } + // result == loadBlockIrrelevant. Enforce the lower bound here + // since don't want to bother moving to the previous block if lower + // bound is already exceeded. Note that the previous block starts with + // keys <= key.UserKey since even though this is the current block's + // separator, the same user key can span multiple blocks. + if i.lower != nil && i.cmp(key.UserKey, i.lower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{} + } + continue + } + key, val := i.data.Last() + if key == nil { + return nil, base.LazyValue{} + } + if i.blockLower != nil && i.cmp(key.UserKey, i.blockLower) < 0 { + i.exhaustedBounds = -1 + return nil, base.LazyValue{} + } + return i.maybeVerifyKey(key, val) + } + return nil, base.LazyValue{} +} + +// Error implements internalIterator.Error, as documented in the pebble +// package. +func (i *singleLevelIterator) Error() error { + if err := i.data.Error(); err != nil { + return err + } + return i.err +} + +// MaybeFilteredKeys may be called when an iterator is exhausted to indicate +// whether or not the last positioning method may have skipped any keys due to +// block-property filters. +func (i *singleLevelIterator) MaybeFilteredKeys() bool { + return i.maybeFilteredKeysSingleLevel +} + +// SetCloseHook sets a function that will be called when the iterator is +// closed. +func (i *singleLevelIterator) SetCloseHook(fn func(i Iterator) error) { + i.closeHook = fn +} + +func firstError(err0, err1 error) error { + if err0 != nil { + return err0 + } + return err1 +} + +// Close implements internalIterator.Close, as documented in the pebble +// package. +func (i *singleLevelIterator) Close() error { + var err error + if i.closeHook != nil { + err = firstError(err, i.closeHook(i)) + } + err = firstError(err, i.data.Close()) + err = firstError(err, i.index.Close()) + if i.dataRH != nil { + err = firstError(err, i.dataRH.Close()) + i.dataRH = nil + } + err = firstError(err, i.err) + if i.bpfs != nil { + releaseBlockPropertiesFilterer(i.bpfs) + } + if i.vbReader != nil { + i.vbReader.close() + } + if i.vbRH != nil { + err = firstError(err, i.vbRH.Close()) + i.vbRH = nil + } + *i = i.resetForReuse() + singleLevelIterPool.Put(i) + return err +} + +func (i *singleLevelIterator) String() string { + if i.vState != nil { + return i.vState.fileNum.String() + } + return i.reader.fileNum.String() +} diff --git a/vendor/github.com/cockroachdb/pebble/sstable/reader_iter_two_lvl.go b/vendor/github.com/cockroachdb/pebble/sstable/reader_iter_two_lvl.go new file mode 100644 index 0000000..ed7cb2e --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/sstable/reader_iter_two_lvl.go @@ -0,0 +1,1084 @@ +// Copyright 2011 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 sstable + +import ( + "context" + "fmt" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" +) + +type twoLevelIterator struct { + singleLevelIterator + // maybeFilteredKeysSingleLevel indicates whether the last iterator + // positioning operation may have skipped any index blocks due to + // block-property filters when positioning the top-level-index. + maybeFilteredKeysTwoLevel bool + topLevelIndex blockIter +} + +// twoLevelIterator implements the base.InternalIterator interface. +var _ base.InternalIterator = (*twoLevelIterator)(nil) + +// loadIndex loads the index block at the current top level index position and +// leaves i.index unpositioned. If unsuccessful, it gets i.err to any error +// encountered, which may be nil if we have simply exhausted the entire table. +// This is used for two level indexes. +func (i *twoLevelIterator) loadIndex(dir int8) loadBlockResult { + // Ensure the index data block iterators are invalidated even if loading of + // the index fails. + i.data.invalidate() + i.index.invalidate() + if !i.topLevelIndex.valid() { + i.index.offset = 0 + i.index.restarts = 0 + return loadBlockFailed + } + v := i.topLevelIndex.value() + bhp, err := decodeBlockHandleWithProperties(v.InPlaceValue()) + if err != nil { + i.err = base.CorruptionErrorf("pebble/table: corrupt top level index entry") + return loadBlockFailed + } + if i.bpfs != nil { + intersects, err := i.bpfs.intersects(bhp.Props) + if err != nil { + i.err = errCorruptIndexEntry + return loadBlockFailed + } + if intersects == blockMaybeExcluded { + intersects = i.resolveMaybeExcluded(dir) + } + if intersects == blockExcluded { + i.maybeFilteredKeysTwoLevel = true + return loadBlockIrrelevant + } + // blockIntersects + } + ctx := objiotracing.WithBlockType(i.ctx, objiotracing.MetadataBlock) + indexBlock, err := i.reader.readBlock(ctx, bhp.BlockHandle, nil /* transform */, nil /* readHandle */, i.stats, i.bufferPool) + if err != nil { + i.err = err + return loadBlockFailed + } + if i.err = i.index.initHandle(i.cmp, indexBlock, i.reader.Properties.GlobalSeqNum, false); i.err == nil { + return loadBlockOK + } + return loadBlockFailed +} + +// resolveMaybeExcluded is invoked when the block-property filterer has found +// that an index block is excluded according to its properties but only if its +// bounds fall within the filter's current bounds. This function consults the +// apprioriate bound, depending on the iteration direction, and returns either +// `blockIntersects` or +// `blockMaybeExcluded`. +func (i *twoLevelIterator) resolveMaybeExcluded(dir int8) intersectsResult { + // This iterator is configured with a bound-limited block property filter. + // The bpf determined this entire index block could be excluded from + // iteration based on the property encoded in the block handle. However, we + // still need to determine if the index block is wholly contained within the + // filter's key bounds. + // + // External guarantees ensure all its data blocks' keys are ≥ the filter's + // lower bound during forward iteration, and that all its data blocks' keys + // are < the filter's upper bound during backward iteration. We only need to + // determine if the opposite bound is also met. + // + // The index separator in topLevelIndex.Key() provides an inclusive + // upper-bound for the index block's keys, guaranteeing that all its keys + // are ≤ topLevelIndex.Key(). For forward iteration, this is all we need. + if dir > 0 { + // Forward iteration. + if i.bpfs.boundLimitedFilter.KeyIsWithinUpperBound(i.topLevelIndex.Key().UserKey) { + return blockExcluded + } + return blockIntersects + } + + // Reverse iteration. + // + // Because we're iterating in the reverse direction, we don't yet have + // enough context available to determine if the block is wholly contained + // within its bounds. This case arises only during backward iteration, + // because of the way the index is structured. + // + // Consider a bound-limited bpf limited to the bounds [b,d), loading the + // block with separator `c`. During reverse iteration, the guarantee that + // all the block's keys are < `d` is externally provided, but no guarantee + // is made on the bpf's lower bound. The separator `c` only provides an + // inclusive upper bound on the block's keys, indicating that the + // corresponding block handle points to a block containing only keys ≤ `c`. + // + // To establish a lower bound, we step the top-level index backwards to read + // the previous block's separator, which provides an inclusive lower bound + // on the original index block's keys. Afterwards, we step forward to + // restore our top-level index position. + if peekKey, _ := i.topLevelIndex.Prev(); peekKey == nil { + // The original block points to the first index block of this table. If + // we knew the lower bound for the entire table, it could provide a + // lower bound, but the code refactoring necessary to read it doesn't + // seem worth the payoff. We fall through to loading the block. + } else if i.bpfs.boundLimitedFilter.KeyIsWithinLowerBound(peekKey.UserKey) { + // The lower-bound on the original index block falls within the filter's + // bounds, and we can skip the block (after restoring our current + // top-level index position). + _, _ = i.topLevelIndex.Next() + return blockExcluded + } + _, _ = i.topLevelIndex.Next() + return blockIntersects +} + +// Note that lower, upper passed into init has nothing to do with virtual sstable +// bounds. If the virtualState passed in is not nil, then virtual sstable bounds +// will be enforced. +func (i *twoLevelIterator) init( + ctx context.Context, + r *Reader, + v *virtualState, + lower, upper []byte, + filterer *BlockPropertiesFilterer, + useFilter, hideObsoletePoints bool, + stats *base.InternalIteratorStats, + rp ReaderProvider, + bufferPool *BufferPool, +) error { + if r.err != nil { + return r.err + } + topLevelIndexH, err := r.readIndex(ctx, stats) + if err != nil { + return err + } + if v != nil { + i.vState = v + // Note that upper is exclusive here. + i.endKeyInclusive, lower, upper = v.constrainBounds(lower, upper, false /* endInclusive */) + } + + i.ctx = ctx + i.lower = lower + i.upper = upper + i.bpfs = filterer + i.useFilter = useFilter + i.reader = r + i.cmp = r.Compare + i.stats = stats + i.hideObsoletePoints = hideObsoletePoints + i.bufferPool = bufferPool + err = i.topLevelIndex.initHandle(i.cmp, topLevelIndexH, r.Properties.GlobalSeqNum, false) + if err != nil { + // blockIter.Close releases topLevelIndexH and always returns a nil error + _ = i.topLevelIndex.Close() + return err + } + i.dataRH = r.readable.NewReadHandle(ctx) + if r.tableFormat >= TableFormatPebblev3 { + if r.Properties.NumValueBlocks > 0 { + i.vbReader = &valueBlockReader{ + ctx: ctx, + bpOpen: i, + rp: rp, + vbih: r.valueBIH, + stats: stats, + } + i.data.lazyValueHandling.vbr = i.vbReader + i.vbRH = r.readable.NewReadHandle(ctx) + } + i.data.lazyValueHandling.hasValuePrefix = true + } + return nil +} + +func (i *twoLevelIterator) String() string { + if i.vState != nil { + return i.vState.fileNum.String() + } + return i.reader.fileNum.String() +} + +// MaybeFilteredKeys may be called when an iterator is exhausted to indicate +// whether or not the last positioning method may have skipped any keys due to +// block-property filters. +func (i *twoLevelIterator) MaybeFilteredKeys() bool { + // While reading sstables with two-level indexes, knowledge of whether we've + // filtered keys is tracked separately for each index level. The + // seek-using-next optimizations have different criteria. We can only reset + // maybeFilteredKeys back to false during a seek when NOT using the + // fast-path that uses the current iterator position. + // + // If either level might have filtered keys to arrive at the current + // iterator position, return MaybeFilteredKeys=true. + return i.maybeFilteredKeysTwoLevel || i.maybeFilteredKeysSingleLevel +} + +// SeekGE implements internalIterator.SeekGE, as documented in the pebble +// package. Note that SeekGE only checks the upper bound. It is up to the +// caller to ensure that key is greater than or equal to the lower bound. +func (i *twoLevelIterator) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + if i.vState != nil { + // Callers of SeekGE don't know about virtual sstable bounds, so we may + // have to internally restrict the bounds. + // + // TODO(bananabrick): We can optimize away this check for the level iter + // if necessary. + if i.cmp(key, i.lower) < 0 { + key = i.lower + } + } + + err := i.err + i.err = nil // clear cached iteration error + + // The twoLevelIterator could be already exhausted. Utilize that when + // trySeekUsingNext is true. See the comment about data-exhausted, PGDE, and + // bounds-exhausted near the top of the file. + if flags.TrySeekUsingNext() && + (i.exhaustedBounds == +1 || (i.data.isDataInvalidated() && i.index.isDataInvalidated())) && + err == nil { + // Already exhausted, so return nil. + return nil, base.LazyValue{} + } + + // SeekGE performs various step-instead-of-seeking optimizations: eg enabled + // by trySeekUsingNext, or by monotonically increasing bounds (i.boundsCmp). + // Care must be taken to ensure that when performing these optimizations and + // the iterator becomes exhausted, i.maybeFilteredKeys is set appropriately. + // Consider a previous SeekGE that filtered keys from k until the current + // iterator position. + // + // If the previous SeekGE exhausted the iterator while seeking within the + // two-level index, it's possible keys greater than or equal to the current + // search key were filtered through skipped index blocks. We must not reuse + // the position of the two-level index iterator without remembering the + // previous value of maybeFilteredKeys. + + // We fall into the slow path if i.index.isDataInvalidated() even if the + // top-level iterator is already positioned correctly and all other + // conditions are met. An alternative structure could reuse topLevelIndex's + // current position and reload the index block to which it points. Arguably, + // an index block load is expensive and the index block may still be earlier + // than the index block containing the sought key, resulting in a wasteful + // block load. + + var dontSeekWithinSingleLevelIter bool + if i.topLevelIndex.isDataInvalidated() || !i.topLevelIndex.valid() || i.index.isDataInvalidated() || err != nil || + (i.boundsCmp <= 0 && !flags.TrySeekUsingNext()) || i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 { + // Slow-path: need to position the topLevelIndex. + + // The previous exhausted state of singleLevelIterator is no longer + // relevant, since we may be moving to a different index block. + i.exhaustedBounds = 0 + i.maybeFilteredKeysTwoLevel = false + flags = flags.DisableTrySeekUsingNext() + var ikey *InternalKey + if ikey, _ = i.topLevelIndex.SeekGE(key, flags); ikey == nil { + i.data.invalidate() + i.index.invalidate() + return nil, base.LazyValue{} + } + + result := i.loadIndex(+1) + if result == loadBlockFailed { + i.boundsCmp = 0 + return nil, base.LazyValue{} + } + if result == loadBlockIrrelevant { + // Enforce the upper bound here since don't want to bother moving + // to the next entry in the top level index if upper bound is + // already exceeded. Note that the next entry starts with keys >= + // ikey.UserKey since even though this is the block separator, the + // same user key can span multiple index blocks. If upper is + // exclusive we use >= below, else we use >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + } + } + // Fall through to skipForward. + dontSeekWithinSingleLevelIter = true + // Clear boundsCmp. + // + // In the typical cases where dontSeekWithinSingleLevelIter=false, + // the singleLevelIterator.SeekGE call will clear boundsCmp. + // However, in this case where dontSeekWithinSingleLevelIter=true, + // we never seek on the single-level iterator. This call will fall + // through to skipForward, which may improperly leave boundsCmp=+1 + // unless we clear it here. + i.boundsCmp = 0 + } + } else { + // INVARIANT: err == nil. + // + // Else fast-path: There are two possible cases, from + // (i.boundsCmp > 0 || flags.TrySeekUsingNext()): + // + // 1) The bounds have moved forward (i.boundsCmp > 0) and this SeekGE is + // respecting the lower bound (guaranteed by Iterator). We know that the + // iterator must already be positioned within or just outside the previous + // bounds. Therefore, the topLevelIndex iter cannot be positioned at an + // entry ahead of the seek position (though it can be positioned behind). + // The !i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 confirms that it is + // not behind. Since it is not ahead and not behind it must be at the + // right position. + // + // 2) This SeekGE will land on a key that is greater than the key we are + // currently at (guaranteed by trySeekUsingNext), but since i.cmp(key, + // i.topLevelIndex.Key().UserKey) <= 0, we are at the correct lower level + // index block. No need to reset the state of singleLevelIterator. + // + // Note that cases 1 and 2 never overlap, and one of them must be true, + // but we have some test code (TestIterRandomizedMaybeFilteredKeys) that + // sets both to true, so we fix things here and then do an invariant + // check. + // + // This invariant checking is important enough that we do not gate it + // behind invariants.Enabled. + if i.boundsCmp > 0 { + // TODO(sumeer): fix TestIterRandomizedMaybeFilteredKeys so as to not + // need this behavior. + flags = flags.DisableTrySeekUsingNext() + } + if i.boundsCmp > 0 == flags.TrySeekUsingNext() { + panic(fmt.Sprintf("inconsistency in optimization case 1 %t and case 2 %t", + i.boundsCmp > 0, flags.TrySeekUsingNext())) + } + + if !flags.TrySeekUsingNext() { + // Case 1. Bounds have changed so the previous exhausted bounds state is + // irrelevant. + // WARNING-data-exhausted: this is safe to do only because the monotonic + // bounds optimizations only work when !data-exhausted. If they also + // worked with data-exhausted, we have made it unclear whether + // data-exhausted is actually true. See the comment at the top of the + // file. + i.exhaustedBounds = 0 + } + // Else flags.TrySeekUsingNext(). The i.exhaustedBounds is important to + // preserve for singleLevelIterator, and twoLevelIterator.skipForward. See + // bug https://github.com/cockroachdb/pebble/issues/2036. + } + + if !dontSeekWithinSingleLevelIter { + // Note that while trySeekUsingNext could be false here, singleLevelIterator + // could do its own boundsCmp-based optimization to seek using next. + if ikey, val := i.singleLevelIterator.SeekGE(key, flags); ikey != nil { + return ikey, val + } + } + return i.skipForward() +} + +// SeekPrefixGE implements internalIterator.SeekPrefixGE, as documented in the +// pebble package. Note that SeekPrefixGE only checks the upper bound. It is up +// to the caller to ensure that key is greater than or equal to the lower bound. +func (i *twoLevelIterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + if i.vState != nil { + // Callers of SeekGE don't know about virtual sstable bounds, so we may + // have to internally restrict the bounds. + // + // TODO(bananabrick): We can optimize away this check for the level iter + // if necessary. + if i.cmp(key, i.lower) < 0 { + key = i.lower + } + } + + // NOTE: prefix is only used for bloom filter checking and not later work in + // this method. Hence, we can use the existing iterator position if the last + // SeekPrefixGE did not fail bloom filter matching. + + err := i.err + i.err = nil // clear cached iteration error + + // The twoLevelIterator could be already exhausted. Utilize that when + // trySeekUsingNext is true. See the comment about data-exhausted, PGDE, and + // bounds-exhausted near the top of the file. + filterUsedAndDidNotMatch := + i.reader.tableFilter != nil && i.useFilter && !i.lastBloomFilterMatched + if flags.TrySeekUsingNext() && !filterUsedAndDidNotMatch && + (i.exhaustedBounds == +1 || (i.data.isDataInvalidated() && i.index.isDataInvalidated())) && + err == nil { + // Already exhausted, so return nil. + return nil, base.LazyValue{} + } + + // Check prefix bloom filter. + if i.reader.tableFilter != nil && i.useFilter { + if !i.lastBloomFilterMatched { + // Iterator is not positioned based on last seek. + flags = flags.DisableTrySeekUsingNext() + } + i.lastBloomFilterMatched = false + var dataH bufferHandle + dataH, i.err = i.reader.readFilter(i.ctx, i.stats) + if i.err != nil { + i.data.invalidate() + return nil, base.LazyValue{} + } + mayContain := i.reader.tableFilter.mayContain(dataH.Get(), prefix) + dataH.Release() + if !mayContain { + // This invalidation may not be necessary for correctness, and may + // be a place to optimize later by reusing the already loaded + // block. It was necessary in earlier versions of the code since + // the caller was allowed to call Next when SeekPrefixGE returned + // nil. This is no longer allowed. + i.data.invalidate() + return nil, base.LazyValue{} + } + i.lastBloomFilterMatched = true + } + + // Bloom filter matches. + + // SeekPrefixGE performs various step-instead-of-seeking optimizations: eg + // enabled by trySeekUsingNext, or by monotonically increasing bounds + // (i.boundsCmp). Care must be taken to ensure that when performing these + // optimizations and the iterator becomes exhausted, + // i.maybeFilteredKeysTwoLevel is set appropriately. Consider a previous + // SeekPrefixGE that filtered keys from k until the current iterator + // position. + // + // If the previous SeekPrefixGE exhausted the iterator while seeking within + // the two-level index, it's possible keys greater than or equal to the + // current search key were filtered through skipped index blocks. We must + // not reuse the position of the two-level index iterator without + // remembering the previous value of maybeFilteredKeysTwoLevel. + + // We fall into the slow path if i.index.isDataInvalidated() even if the + // top-level iterator is already positioned correctly and all other + // conditions are met. An alternative structure could reuse topLevelIndex's + // current position and reload the index block to which it points. Arguably, + // an index block load is expensive and the index block may still be earlier + // than the index block containing the sought key, resulting in a wasteful + // block load. + + var dontSeekWithinSingleLevelIter bool + if i.topLevelIndex.isDataInvalidated() || !i.topLevelIndex.valid() || i.index.isDataInvalidated() || err != nil || + (i.boundsCmp <= 0 && !flags.TrySeekUsingNext()) || i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 { + // Slow-path: need to position the topLevelIndex. + + // The previous exhausted state of singleLevelIterator is no longer + // relevant, since we may be moving to a different index block. + i.exhaustedBounds = 0 + i.maybeFilteredKeysTwoLevel = false + flags = flags.DisableTrySeekUsingNext() + var ikey *InternalKey + if ikey, _ = i.topLevelIndex.SeekGE(key, flags); ikey == nil { + i.data.invalidate() + i.index.invalidate() + return nil, base.LazyValue{} + } + + result := i.loadIndex(+1) + if result == loadBlockFailed { + i.boundsCmp = 0 + return nil, base.LazyValue{} + } + if result == loadBlockIrrelevant { + // Enforce the upper bound here since don't want to bother moving + // to the next entry in the top level index if upper bound is + // already exceeded. Note that the next entry starts with keys >= + // ikey.UserKey since even though this is the block separator, the + // same user key can span multiple index blocks. If upper is + // exclusive we use >= below, else we use >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + } + } + // Fall through to skipForward. + dontSeekWithinSingleLevelIter = true + // Clear boundsCmp. + // + // In the typical cases where dontSeekWithinSingleLevelIter=false, + // the singleLevelIterator.SeekPrefixGE call will clear boundsCmp. + // However, in this case where dontSeekWithinSingleLevelIter=true, + // we never seek on the single-level iterator. This call will fall + // through to skipForward, which may improperly leave boundsCmp=+1 + // unless we clear it here. + i.boundsCmp = 0 + } + } else { + // INVARIANT: err == nil. + // + // Else fast-path: There are two possible cases, from + // (i.boundsCmp > 0 || flags.TrySeekUsingNext()): + // + // 1) The bounds have moved forward (i.boundsCmp > 0) and this + // SeekPrefixGE is respecting the lower bound (guaranteed by Iterator). We + // know that the iterator must already be positioned within or just + // outside the previous bounds. Therefore, the topLevelIndex iter cannot + // be positioned at an entry ahead of the seek position (though it can be + // positioned behind). The !i.cmp(key, i.topLevelIndex.Key().UserKey) > 0 + // confirms that it is not behind. Since it is not ahead and not behind it + // must be at the right position. + // + // 2) This SeekPrefixGE will land on a key that is greater than the key we + // are currently at (guaranteed by trySeekUsingNext), but since i.cmp(key, + // i.topLevelIndex.Key().UserKey) <= 0, we are at the correct lower level + // index block. No need to reset the state of singleLevelIterator. + // + // Note that cases 1 and 2 never overlap, and one of them must be true. + // This invariant checking is important enough that we do not gate it + // behind invariants.Enabled. + if i.boundsCmp > 0 == flags.TrySeekUsingNext() { + panic(fmt.Sprintf("inconsistency in optimization case 1 %t and case 2 %t", + i.boundsCmp > 0, flags.TrySeekUsingNext())) + } + + if !flags.TrySeekUsingNext() { + // Case 1. Bounds have changed so the previous exhausted bounds state is + // irrelevant. + // WARNING-data-exhausted: this is safe to do only because the monotonic + // bounds optimizations only work when !data-exhausted. If they also + // worked with data-exhausted, we have made it unclear whether + // data-exhausted is actually true. See the comment at the top of the + // file. + i.exhaustedBounds = 0 + } + // Else flags.TrySeekUsingNext(). The i.exhaustedBounds is important to + // preserve for singleLevelIterator, and twoLevelIterator.skipForward. See + // bug https://github.com/cockroachdb/pebble/issues/2036. + } + + if !dontSeekWithinSingleLevelIter { + if ikey, val := i.singleLevelIterator.seekPrefixGE( + prefix, key, flags, false /* checkFilter */); ikey != nil { + return ikey, val + } + } + // NB: skipForward checks whether exhaustedBounds is already +1. + return i.skipForward() +} + +// virtualLast should only be called if i.vReader != nil and i.endKeyInclusive +// is true. +func (i *twoLevelIterator) virtualLast() (*InternalKey, base.LazyValue) { + if i.vState == nil { + panic("pebble: invalid call to virtualLast") + } + + // Seek to the first internal key. + ikey, _ := i.SeekGE(i.upper, base.SeekGEFlagsNone) + if i.endKeyInclusive { + // Let's say the virtual sstable upper bound is c#1, with the keys c#3, c#2, + // c#1, d, e, ... in the sstable. So, the last key in the virtual sstable is + // c#1. We can perform SeekGE(i.upper) and then keep nexting until we find + // the last key with userkey == i.upper. + // + // TODO(bananabrick): Think about how to improve this. If many internal keys + // with the same user key at the upper bound then this could be slow, but + // maybe the odds of having many internal keys with the same user key at the + // upper bound are low. + for ikey != nil && i.cmp(ikey.UserKey, i.upper) == 0 { + ikey, _ = i.Next() + } + return i.Prev() + } + // We seeked to the first key >= i.upper. + return i.Prev() +} + +// SeekLT implements internalIterator.SeekLT, as documented in the pebble +// package. Note that SeekLT only checks the lower bound. It is up to the +// caller to ensure that key is less than the upper bound. +func (i *twoLevelIterator) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*InternalKey, base.LazyValue) { + if i.vState != nil { + // Might have to fix upper bound since virtual sstable bounds are not + // known to callers of SeekLT. + // + // TODO(bananabrick): We can optimize away this check for the level iter + // if necessary. + cmp := i.cmp(key, i.upper) + // key == i.upper is fine. We'll do the right thing and return the + // first internal key with user key < key. + if cmp > 0 { + return i.virtualLast() + } + } + + i.exhaustedBounds = 0 + i.err = nil // clear cached iteration error + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + var result loadBlockResult + var ikey *InternalKey + // NB: Unlike SeekGE, we don't have a fast-path here since we don't know + // whether the topLevelIndex is positioned after the position that would + // be returned by doing i.topLevelIndex.SeekGE(). To know this we would + // need to know the index key preceding the current one. + // NB: If a bound-limited block property filter is configured, it's + // externally ensured that the filter is disabled (through returning + // Intersects=false irrespective of the block props provided) during seeks. + i.maybeFilteredKeysTwoLevel = false + if ikey, _ = i.topLevelIndex.SeekGE(key, base.SeekGEFlagsNone); ikey == nil { + if ikey, _ = i.topLevelIndex.Last(); ikey == nil { + i.data.invalidate() + i.index.invalidate() + return nil, base.LazyValue{} + } + + result = i.loadIndex(-1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.singleLevelIterator.lastInternal(); ikey != nil { + return i.maybeVerifyKey(ikey, val) + } + // Fall through to skipBackward since the singleLevelIterator did + // not have any blocks that satisfy the block interval + // constraints, or the lower bound was reached. + } + // Else loadBlockIrrelevant, so fall through. + } else { + result = i.loadIndex(-1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.singleLevelIterator.SeekLT(key, flags); ikey != nil { + return i.maybeVerifyKey(ikey, val) + } + // Fall through to skipBackward since the singleLevelIterator did + // not have any blocks that satisfy the block interval + // constraint, or the lower bound was reached. + } + // Else loadBlockIrrelevant, so fall through. + } + if result == loadBlockIrrelevant { + // Enforce the lower bound here since don't want to bother moving to + // the previous entry in the top level index if lower bound is already + // exceeded. Note that the previous entry starts with keys <= + // ikey.UserKey since even though this is the current block's + // separator, the same user key can span multiple index blocks. + if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { + i.exhaustedBounds = -1 + } + } + // NB: skipBackward checks whether exhaustedBounds is already -1. + return i.skipBackward() +} + +// First implements internalIterator.First, as documented in the pebble +// package. Note that First only checks the upper bound. It is up to the caller +// to ensure that key is greater than or equal to the lower bound (e.g. via a +// call to SeekGE(lower)). +func (i *twoLevelIterator) First() (*InternalKey, base.LazyValue) { + // If the iterator was created on a virtual sstable, we will SeekGE to the + // lower bound instead of using First, because First does not respect + // bounds. + if i.vState != nil { + return i.SeekGE(i.lower, base.SeekGEFlagsNone) + } + + if i.lower != nil { + panic("twoLevelIterator.First() used despite lower bound") + } + i.exhaustedBounds = 0 + i.maybeFilteredKeysTwoLevel = false + i.err = nil // clear cached iteration error + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + var ikey *InternalKey + if ikey, _ = i.topLevelIndex.First(); ikey == nil { + return nil, base.LazyValue{} + } + + result := i.loadIndex(+1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.singleLevelIterator.First(); ikey != nil { + return ikey, val + } + // Else fall through to skipForward. + } else { + // result == loadBlockIrrelevant. Enforce the upper bound here since + // don't want to bother moving to the next entry in the top level + // index if upper bound is already exceeded. Note that the next entry + // starts with keys >= ikey.UserKey since even though this is the + // block separator, the same user key can span multiple index blocks. + // If upper is exclusive we use >= below, else we use >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + } + } + } + // NB: skipForward checks whether exhaustedBounds is already +1. + return i.skipForward() +} + +// Last implements internalIterator.Last, as documented in the pebble +// package. Note that Last only checks the lower bound. It is up to the caller +// to ensure that key is less than the upper bound (e.g. via a call to +// SeekLT(upper)) +func (i *twoLevelIterator) Last() (*InternalKey, base.LazyValue) { + if i.vState != nil { + if i.endKeyInclusive { + return i.virtualLast() + } + return i.SeekLT(i.upper, base.SeekLTFlagsNone) + } + + if i.upper != nil { + panic("twoLevelIterator.Last() used despite upper bound") + } + i.exhaustedBounds = 0 + i.maybeFilteredKeysTwoLevel = false + i.err = nil // clear cached iteration error + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + + var ikey *InternalKey + if ikey, _ = i.topLevelIndex.Last(); ikey == nil { + return nil, base.LazyValue{} + } + + result := i.loadIndex(-1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.singleLevelIterator.Last(); ikey != nil { + return ikey, val + } + // Else fall through to skipBackward. + } else { + // result == loadBlockIrrelevant. Enforce the lower bound here + // since don't want to bother moving to the previous entry in the + // top level index if lower bound is already exceeded. Note that + // the previous entry starts with keys <= ikey.UserKey since even + // though this is the current block's separator, the same user key + // can span multiple index blocks. + if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { + i.exhaustedBounds = -1 + } + } + // NB: skipBackward checks whether exhaustedBounds is already -1. + return i.skipBackward() +} + +// Next implements internalIterator.Next, as documented in the pebble +// package. +// Note: twoLevelCompactionIterator.Next mirrors the implementation of +// twoLevelIterator.Next due to performance. Keep the two in sync. +func (i *twoLevelIterator) Next() (*InternalKey, base.LazyValue) { + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + i.maybeFilteredKeysTwoLevel = false + if i.err != nil { + return nil, base.LazyValue{} + } + if key, val := i.singleLevelIterator.Next(); key != nil { + return key, val + } + return i.skipForward() +} + +// NextPrefix implements (base.InternalIterator).NextPrefix. +func (i *twoLevelIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { + if i.exhaustedBounds == +1 { + panic("Next called even though exhausted upper bound") + } + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + i.maybeFilteredKeysTwoLevel = false + if i.err != nil { + return nil, base.LazyValue{} + } + if key, val := i.singleLevelIterator.NextPrefix(succKey); key != nil { + return key, val + } + // key == nil + if i.err != nil { + return nil, base.LazyValue{} + } + + // Did not find prefix in the existing second-level index block. This is the + // slow-path where we seek the iterator. + var ikey *InternalKey + if ikey, _ = i.topLevelIndex.SeekGE(succKey, base.SeekGEFlagsNone); ikey == nil { + i.data.invalidate() + i.index.invalidate() + return nil, base.LazyValue{} + } + result := i.loadIndex(+1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockIrrelevant { + // Enforce the upper bound here since don't want to bother moving to the + // next entry in the top level index if upper bound is already exceeded. + // Note that the next entry starts with keys >= ikey.UserKey since even + // though this is the block separator, the same user key can span multiple + // index blocks. If upper is exclusive we use >= below, else we use >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + } + } + } else if key, val := i.singleLevelIterator.SeekGE(succKey, base.SeekGEFlagsNone); key != nil { + return i.maybeVerifyKey(key, val) + } + return i.skipForward() +} + +// Prev implements internalIterator.Prev, as documented in the pebble +// package. +func (i *twoLevelIterator) Prev() (*InternalKey, base.LazyValue) { + // Seek optimization only applies until iterator is first positioned after SetBounds. + i.boundsCmp = 0 + i.maybeFilteredKeysTwoLevel = false + if i.err != nil { + return nil, base.LazyValue{} + } + if key, val := i.singleLevelIterator.Prev(); key != nil { + return key, val + } + return i.skipBackward() +} + +func (i *twoLevelIterator) skipForward() (*InternalKey, base.LazyValue) { + for { + if i.err != nil || i.exhaustedBounds > 0 { + return nil, base.LazyValue{} + } + i.exhaustedBounds = 0 + var ikey *InternalKey + if ikey, _ = i.topLevelIndex.Next(); ikey == nil { + i.data.invalidate() + i.index.invalidate() + return nil, base.LazyValue{} + } + result := i.loadIndex(+1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.singleLevelIterator.firstInternal(); ikey != nil { + return i.maybeVerifyKey(ikey, val) + } + // Next iteration will return if singleLevelIterator set + // exhaustedBounds = +1. + } else { + // result == loadBlockIrrelevant. Enforce the upper bound here + // since don't want to bother moving to the next entry in the top + // level index if upper bound is already exceeded. Note that the + // next entry starts with keys >= ikey.UserKey since even though + // this is the block separator, the same user key can span + // multiple index blocks. If upper is exclusive we use >= + // below, else we use >. + if i.upper != nil { + cmp := i.cmp(ikey.UserKey, i.upper) + if (!i.endKeyInclusive && cmp >= 0) || cmp > 0 { + i.exhaustedBounds = +1 + // Next iteration will return. + } + } + } + } +} + +func (i *twoLevelIterator) skipBackward() (*InternalKey, base.LazyValue) { + for { + if i.err != nil || i.exhaustedBounds < 0 { + return nil, base.LazyValue{} + } + i.exhaustedBounds = 0 + var ikey *InternalKey + if ikey, _ = i.topLevelIndex.Prev(); ikey == nil { + i.data.invalidate() + i.index.invalidate() + return nil, base.LazyValue{} + } + result := i.loadIndex(-1) + if result == loadBlockFailed { + return nil, base.LazyValue{} + } + if result == loadBlockOK { + if ikey, val := i.singleLevelIterator.lastInternal(); ikey != nil { + return i.maybeVerifyKey(ikey, val) + } + // Next iteration will return if singleLevelIterator set + // exhaustedBounds = -1. + } else { + // result == loadBlockIrrelevant. Enforce the lower bound here + // since don't want to bother moving to the previous entry in the + // top level index if lower bound is already exceeded. Note that + // the previous entry starts with keys <= ikey.UserKey since even + // though this is the current block's separator, the same user key + // can span multiple index blocks. + if i.lower != nil && i.cmp(ikey.UserKey, i.lower) < 0 { + i.exhaustedBounds = -1 + // Next iteration will return. + } + } + } +} + +// Close implements internalIterator.Close, as documented in the pebble +// package. +func (i *twoLevelIterator) Close() error { + var err error + if i.closeHook != nil { + err = firstError(err, i.closeHook(i)) + } + err = firstError(err, i.data.Close()) + err = firstError(err, i.index.Close()) + err = firstError(err, i.topLevelIndex.Close()) + if i.dataRH != nil { + err = firstError(err, i.dataRH.Close()) + i.dataRH = nil + } + err = firstError(err, i.err) + if i.bpfs != nil { + releaseBlockPropertiesFilterer(i.bpfs) + } + if i.vbReader != nil { + i.vbReader.close() + } + if i.vbRH != nil { + err = firstError(err, i.vbRH.Close()) + i.vbRH = nil + } + *i = twoLevelIterator{ + singleLevelIterator: i.singleLevelIterator.resetForReuse(), + topLevelIndex: i.topLevelIndex.resetForReuse(), + } + twoLevelIterPool.Put(i) + return err +} + +// Note: twoLevelCompactionIterator and compactionIterator are very similar but +// were separated due to performance. +type twoLevelCompactionIterator struct { + *twoLevelIterator + bytesIterated *uint64 + prevOffset uint64 +} + +// twoLevelCompactionIterator implements the base.InternalIterator interface. +var _ base.InternalIterator = (*twoLevelCompactionIterator)(nil) + +func (i *twoLevelCompactionIterator) Close() error { + return i.twoLevelIterator.Close() +} + +func (i *twoLevelCompactionIterator) SeekGE( + key []byte, flags base.SeekGEFlags, +) (*InternalKey, base.LazyValue) { + panic("pebble: SeekGE unimplemented") +} + +func (i *twoLevelCompactionIterator) SeekPrefixGE( + prefix, key []byte, flags base.SeekGEFlags, +) (*base.InternalKey, base.LazyValue) { + panic("pebble: SeekPrefixGE unimplemented") +} + +func (i *twoLevelCompactionIterator) SeekLT( + key []byte, flags base.SeekLTFlags, +) (*InternalKey, base.LazyValue) { + panic("pebble: SeekLT unimplemented") +} + +func (i *twoLevelCompactionIterator) First() (*InternalKey, base.LazyValue) { + i.err = nil // clear cached iteration error + return i.skipForward(i.twoLevelIterator.First()) +} + +func (i *twoLevelCompactionIterator) Last() (*InternalKey, base.LazyValue) { + panic("pebble: Last unimplemented") +} + +// Note: twoLevelCompactionIterator.Next mirrors the implementation of +// twoLevelIterator.Next due to performance. Keep the two in sync. +func (i *twoLevelCompactionIterator) Next() (*InternalKey, base.LazyValue) { + if i.err != nil { + return nil, base.LazyValue{} + } + return i.skipForward(i.singleLevelIterator.Next()) +} + +func (i *twoLevelCompactionIterator) NextPrefix(succKey []byte) (*InternalKey, base.LazyValue) { + panic("pebble: NextPrefix unimplemented") +} + +func (i *twoLevelCompactionIterator) Prev() (*InternalKey, base.LazyValue) { + panic("pebble: Prev unimplemented") +} + +func (i *twoLevelCompactionIterator) String() string { + if i.vState != nil { + return i.vState.fileNum.String() + } + return i.reader.fileNum.String() +} + +func (i *twoLevelCompactionIterator) skipForward( + key *InternalKey, val base.LazyValue, +) (*InternalKey, base.LazyValue) { + if key == nil { + for { + if key, _ := i.topLevelIndex.Next(); key == nil { + break + } + result := i.loadIndex(+1) + if result != loadBlockOK { + if i.err != nil { + break + } + switch result { + case loadBlockFailed: + // We checked that i.index was at a valid entry, so + // loadBlockFailed could not have happened due to to i.index + // being exhausted, and must be due to an error. + panic("loadBlock should not have failed with no error") + case loadBlockIrrelevant: + panic("compactionIter should not be using block intervals for skipping") + default: + panic(fmt.Sprintf("unexpected case %d", result)) + } + } + // result == loadBlockOK + if key, val = i.singleLevelIterator.First(); key != nil { + break + } + } + } + + curOffset := i.recordOffset() + *i.bytesIterated += uint64(curOffset - i.prevOffset) + i.prevOffset = curOffset + + if i.vState != nil && key != nil { + cmp := i.cmp(key.UserKey, i.vState.upper.UserKey) + if cmp > 0 || (i.vState.upper.IsExclusiveSentinel() && cmp == 0) { + return nil, base.LazyValue{} + } + } + + return key, val +} diff --git a/vendor/github.com/cockroachdb/pebble/sstable/reader_virtual.go b/vendor/github.com/cockroachdb/pebble/sstable/reader_virtual.go new file mode 100644 index 0000000..b044d19 --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/sstable/reader_virtual.go @@ -0,0 +1,206 @@ +// Copyright 2011 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 sstable + +import ( + "context" + + "github.com/cockroachdb/pebble/internal/base" + "github.com/cockroachdb/pebble/internal/keyspan" + "github.com/cockroachdb/pebble/internal/manifest" +) + +// VirtualReader wraps Reader. Its purpose is to restrict functionality of the +// Reader which should be inaccessible to virtual sstables, and enforce bounds +// invariants associated with virtual sstables. All reads on virtual sstables +// should go through a VirtualReader. +// +// INVARIANT: Any iterators created through a virtual reader will guarantee that +// they don't expose keys outside the virtual sstable bounds. +type VirtualReader struct { + vState virtualState + reader *Reader + Properties CommonProperties +} + +// Lightweight virtual sstable state which can be passed to sstable iterators. +type virtualState struct { + lower InternalKey + upper InternalKey + fileNum base.FileNum + Compare Compare + isForeign bool +} + +func ceilDiv(a, b uint64) uint64 { + return (a + b - 1) / b +} + +// MakeVirtualReader is used to contruct a reader which can read from virtual +// sstables. +func MakeVirtualReader( + reader *Reader, meta manifest.VirtualFileMeta, isForeign bool, +) VirtualReader { + if reader.fileNum != meta.FileBacking.DiskFileNum { + panic("pebble: invalid call to MakeVirtualReader") + } + + vState := virtualState{ + lower: meta.Smallest, + upper: meta.Largest, + fileNum: meta.FileNum, + Compare: reader.Compare, + isForeign: isForeign, + } + v := VirtualReader{ + vState: vState, + reader: reader, + } + + v.Properties.RawKeySize = ceilDiv(reader.Properties.RawKeySize*meta.Size, meta.FileBacking.Size) + v.Properties.RawValueSize = ceilDiv(reader.Properties.RawValueSize*meta.Size, meta.FileBacking.Size) + v.Properties.NumEntries = ceilDiv(reader.Properties.NumEntries*meta.Size, meta.FileBacking.Size) + v.Properties.NumDeletions = ceilDiv(reader.Properties.NumDeletions*meta.Size, meta.FileBacking.Size) + v.Properties.NumRangeDeletions = ceilDiv(reader.Properties.NumRangeDeletions*meta.Size, meta.FileBacking.Size) + v.Properties.NumRangeKeyDels = ceilDiv(reader.Properties.NumRangeKeyDels*meta.Size, meta.FileBacking.Size) + + // Note that we rely on NumRangeKeySets for correctness. If the sstable may + // contain range keys, then NumRangeKeySets must be > 0. ceilDiv works because + // meta.Size will not be 0 for virtual sstables. + v.Properties.NumRangeKeySets = ceilDiv(reader.Properties.NumRangeKeySets*meta.Size, meta.FileBacking.Size) + v.Properties.ValueBlocksSize = ceilDiv(reader.Properties.ValueBlocksSize*meta.Size, meta.FileBacking.Size) + v.Properties.NumSizedDeletions = ceilDiv(reader.Properties.NumSizedDeletions*meta.Size, meta.FileBacking.Size) + v.Properties.RawPointTombstoneKeySize = ceilDiv(reader.Properties.RawPointTombstoneKeySize*meta.Size, meta.FileBacking.Size) + v.Properties.RawPointTombstoneValueSize = ceilDiv(reader.Properties.RawPointTombstoneValueSize*meta.Size, meta.FileBacking.Size) + return v +} + +// NewCompactionIter is the compaction iterator function for virtual readers. +func (v *VirtualReader) NewCompactionIter( + bytesIterated *uint64, rp ReaderProvider, bufferPool *BufferPool, +) (Iterator, error) { + return v.reader.newCompactionIter(bytesIterated, rp, &v.vState, bufferPool) +} + +// NewIterWithBlockPropertyFiltersAndContextEtc wraps +// Reader.NewIterWithBlockPropertyFiltersAndContext. We assume that the passed +// in [lower, upper) bounds will have at least some overlap with the virtual +// sstable bounds. No overlap is not currently supported in the iterator. +func (v *VirtualReader) NewIterWithBlockPropertyFiltersAndContextEtc( + ctx context.Context, + lower, upper []byte, + filterer *BlockPropertiesFilterer, + hideObsoletePoints, useFilterBlock bool, + stats *base.InternalIteratorStats, + rp ReaderProvider, +) (Iterator, error) { + return v.reader.newIterWithBlockPropertyFiltersAndContext( + ctx, lower, upper, filterer, hideObsoletePoints, useFilterBlock, stats, rp, &v.vState, + ) +} + +// ValidateBlockChecksumsOnBacking will call ValidateBlockChecksumsOnBacking on the underlying reader. +// Note that block checksum validation is NOT restricted to virtual sstable bounds. +func (v *VirtualReader) ValidateBlockChecksumsOnBacking() error { + return v.reader.ValidateBlockChecksums() +} + +// NewRawRangeDelIter wraps Reader.NewRawRangeDelIter. +func (v *VirtualReader) NewRawRangeDelIter() (keyspan.FragmentIterator, error) { + iter, err := v.reader.NewRawRangeDelIter() + if err != nil { + return nil, err + } + if iter == nil { + return nil, nil + } + + // Truncation of spans isn't allowed at a user key that also contains points + // in the same virtual sstable, as it would lead to covered points getting + // uncovered. Set panicOnUpperTruncate to true if the file's upper bound + // is not an exclusive sentinel. + // + // As an example, if an sstable contains a rangedel a-c and point keys at + // a.SET.2 and b.SET.3, the file bounds [a#2,SET-b#RANGEDELSENTINEL] are + // allowed (as they exclude b.SET.3), or [a#2,SET-c#RANGEDELSENTINEL] (as it + // includes both point keys), but not [a#2,SET-b#3,SET] (as it would truncate + // the rangedel at b and lead to the point being uncovered). + return keyspan.Truncate( + v.reader.Compare, iter, v.vState.lower.UserKey, v.vState.upper.UserKey, + &v.vState.lower, &v.vState.upper, !v.vState.upper.IsExclusiveSentinel(), /* panicOnUpperTruncate */ + ), nil +} + +// NewRawRangeKeyIter wraps Reader.NewRawRangeKeyIter. +func (v *VirtualReader) NewRawRangeKeyIter() (keyspan.FragmentIterator, error) { + iter, err := v.reader.NewRawRangeKeyIter() + if err != nil { + return nil, err + } + if iter == nil { + return nil, nil + } + + // Truncation of spans isn't allowed at a user key that also contains points + // in the same virtual sstable, as it would lead to covered points getting + // uncovered. Set panicOnUpperTruncate to true if the file's upper bound + // is not an exclusive sentinel. + // + // As an example, if an sstable contains a range key a-c and point keys at + // a.SET.2 and b.SET.3, the file bounds [a#2,SET-b#RANGEKEYSENTINEL] are + // allowed (as they exclude b.SET.3), or [a#2,SET-c#RANGEKEYSENTINEL] (as it + // includes both point keys), but not [a#2,SET-b#3,SET] (as it would truncate + // the range key at b and lead to the point being uncovered). + return keyspan.Truncate( + v.reader.Compare, iter, v.vState.lower.UserKey, v.vState.upper.UserKey, + &v.vState.lower, &v.vState.upper, !v.vState.upper.IsExclusiveSentinel(), /* panicOnUpperTruncate */ + ), nil +} + +// Constrain bounds will narrow the start, end bounds if they do not fit within +// the virtual sstable. The function will return if the new end key is +// inclusive. +func (v *virtualState) constrainBounds( + start, end []byte, endInclusive bool, +) (lastKeyInclusive bool, first []byte, last []byte) { + first = start + if start == nil || v.Compare(start, v.lower.UserKey) < 0 { + first = v.lower.UserKey + } + + // Note that we assume that start, end has some overlap with the virtual + // sstable bounds. + last = v.upper.UserKey + lastKeyInclusive = !v.upper.IsExclusiveSentinel() + if end != nil { + cmp := v.Compare(end, v.upper.UserKey) + switch { + case cmp == 0: + lastKeyInclusive = !v.upper.IsExclusiveSentinel() && endInclusive + last = v.upper.UserKey + case cmp > 0: + lastKeyInclusive = !v.upper.IsExclusiveSentinel() + last = v.upper.UserKey + default: + lastKeyInclusive = endInclusive + last = end + } + } + // TODO(bananabrick): What if someone passes in bounds completely outside of + // virtual sstable bounds? + return lastKeyInclusive, first, last +} + +// EstimateDiskUsage just calls VirtualReader.reader.EstimateDiskUsage after +// enforcing the virtual sstable bounds. +func (v *VirtualReader) EstimateDiskUsage(start, end []byte) (uint64, error) { + _, f, l := v.vState.constrainBounds(start, end, true /* endInclusive */) + return v.reader.EstimateDiskUsage(f, l) +} + +// CommonProperties implements the CommonReader interface. +func (v *VirtualReader) CommonProperties() *CommonProperties { + return &v.Properties +} diff --git a/vendor/github.com/cockroachdb/pebble/sstable/suffix_rewriter.go b/vendor/github.com/cockroachdb/pebble/sstable/suffix_rewriter.go index aee7024..9672ded 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/suffix_rewriter.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/suffix_rewriter.go @@ -2,6 +2,7 @@ package sstable import ( "bytes" + "context" "math" "sync" @@ -42,6 +43,9 @@ func RewriteKeySuffixes( // modification as they are not affected by the suffix, while block and table // properties are only minimally recomputed. // +// TODO(sumeer): document limitations, if any, due to this limited +// re-computation of properties (is there any loss of fidelity?). +// // Any block and table property collectors configured in the WriterOptions must // implement SuffixReplaceableTableCollector/SuffixReplaceableBlockCollector. // @@ -49,6 +53,26 @@ func RewriteKeySuffixes( // same TableFormat as the input, which is returned in case the caller wants // to do some error checking. Suffix rewriting is meant to be efficient, and // allowing changes in the TableFormat detracts from that efficiency. +// +// Any obsolete bits that key-value pairs may be annotated with are ignored +// and lost during the rewrite. Additionally, the output sstable has the +// pebble.obsolete.is_strict property set to false. These limitations could be +// removed if needed. The current use case for +// RewriteKeySuffixesAndReturnFormat in CockroachDB is for MVCC-compliant file +// ingestion, where these files do not contain RANGEDELs and have one +// key-value pair per userkey -- so they trivially satisfy the strict +// criteria, and we don't need the obsolete bit as a performance optimization. +// For disaggregated storage, strict obsolete sstables are needed for L5 and +// L6, but at the time of writing, we expect such MVCC-compliant file +// ingestion to only ingest into levels L4 and higher. If this changes, we can +// do one of two things to get rid of this limitation: +// - Validate that there are no duplicate userkeys and no RANGEDELs/MERGEs +// in the sstable to be rewritten. Validating no duplicate userkeys is +// non-trivial when rewriting blocks in parallel, so we could encode the +// pre-existing condition in the (existing) SnapshotPinnedKeys property -- +// we need to update the external sst writer to calculate and encode this +// property. +// - Preserve the obsolete bit (with changes to the blockIter). func RewriteKeySuffixesAndReturnFormat( sst []byte, rOpts ReaderOptions, @@ -183,7 +207,7 @@ func rewriteBlocks( if err != nil { return err } - if err := iter.init(r.Compare, inputBlock, r.Properties.GlobalSeqNum); err != nil { + if err := iter.init(r.Compare, inputBlock, r.Properties.GlobalSeqNum, false); err != nil { return err } @@ -217,12 +241,12 @@ func rewriteBlocks( copy(scratch.UserKey, key.UserKey[:si]) copy(scratch.UserKey[si:], to) - // NB: for TableFormatPebblev3, since + // NB: for TableFormatPebblev3 and higher, since // !iter.lazyValueHandling.hasValuePrefix, it will return the raw value // in the block, which includes the 1-byte prefix. This is fine since bw // also does not know about the prefix and will preserve it in bw.add. v := val.InPlaceValue() - if invariants.Enabled && r.tableFormat == TableFormatPebblev3 && + if invariants.Enabled && r.tableFormat >= TableFormatPebblev3 && key.Kind() == InternalKeyKindSet { if len(v) < 1 { return errors.Errorf("value has no prefix") @@ -432,8 +456,13 @@ func (c copyFilterWriter) policyName() string { return c.origPolicyName } // RewriteKeySuffixesViaWriter is similar to RewriteKeySuffixes but uses just a // single loop over the Reader that writes each key to the Writer with the new // suffix. The is significantly slower than the parallelized rewriter, and does -// more work to rederive filters, props, etc, however re-doing that work makes -// it less restrictive -- props no longer need to +// more work to rederive filters, props, etc. +// +// Any obsolete bits that key-value pairs may be annotated with are ignored +// and lost during the rewrite. Some of the obsolete bits may be recreated -- +// specifically when there are multiple keys with the same user key. +// Additionally, the output sstable has the pebble.obsolete.is_strict property +// set to false. See the longer comment at RewriteKeySuffixesAndReturnFormat. func RewriteKeySuffixesViaWriter( r *Reader, out objstorage.Writable, o WriterOptions, from, to []byte, ) (*WriterMetadata, error) { @@ -441,6 +470,7 @@ func RewriteKeySuffixesViaWriter( return nil, errors.New("a valid splitter is required to rewrite suffixes") } + o.IsStrictObsolete = false w := NewWriter(out, o) defer func() { if w != nil { @@ -471,7 +501,7 @@ func RewriteKeySuffixesViaWriter( if err != nil { return nil, err } - if w.addPoint(scratch, val); err != nil { + if w.addPoint(scratch, val, false); err != nil { return nil, err } k, v = i.Next() @@ -514,7 +544,7 @@ func readBlockBuf(r *Reader, bh BlockHandle, buf []byte) ([]byte, []byte, error) return res, buf, err } -// memReader is a thin wrapper around a []byte such that it can be passed to an +// memReader is a thin wrapper around a []byte such that it can be passed to // sstable.Reader. It supports concurrent use, and does so without locking in // contrast to the heavier read/write vfs.MemFile. type memReader struct { @@ -534,22 +564,26 @@ func newMemReader(b []byte) *memReader { return r } -// ReadAt implements io.ReaderAt. -func (m *memReader) ReadAt(p []byte, off int64) (n int, err error) { - return m.r.ReadAt(p, off) +// ReadAt is part of objstorage.Readable. +func (m *memReader) ReadAt(_ context.Context, p []byte, off int64) error { + n, err := m.r.ReadAt(p, off) + if invariants.Enabled && err == nil && n != len(p) { + panic("short read") + } + return err } -// Close implements io.Closer. +// Close is part of objstorage.Readable. func (*memReader) Close() error { return nil } -// Stat implements objstorage.Readable. +// Stat is part of objstorage.Readable. func (m *memReader) Size() int64 { return int64(len(m.b)) } -// NewReadHandle implements objstorage.Readable. -func (m *memReader) NewReadHandle() objstorage.ReadHandle { +// NewReadHandle is part of objstorage.Readable. +func (m *memReader) NewReadHandle(_ context.Context) objstorage.ReadHandle { return &m.rh } diff --git a/vendor/github.com/cockroachdb/pebble/sstable/table.go b/vendor/github.com/cockroachdb/pebble/sstable/table.go index 3d96684..040efaf 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/table.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/table.go @@ -68,8 +68,8 @@ package sstable // import "github.com/cockroachdb/pebble/sstable" import ( + "context" "encoding/binary" - "io" "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" @@ -160,8 +160,27 @@ The metaindex block also contains block handles as values, with keys being the names of the meta blocks. For a description of value blocks and the meta value index block, see -value_block.go +value_block.go. +Data blocks have some additional features: +- For TableFormatPebblev3 onwards: + - For SETs, the value has a 1 byte value prefix, which indicates whether the + value is inline, or in a separate value block, and indicates whether the + prefix of the userkey (as defined by split) has changed or not. See + value_block.go for details. + - The most significant bit of the restart points is used to indicate whether + userkey prefix has changed since the last restart point. See the detailed + comment in blockWriter. + - The maximum length of the "shared prefix" when encoding the key, is the + length of the prefix of the userkey (as defined by split) of the previous + key. + +- For TableFormatPebblev4 onwards: + - The key kinds may be altered to set the + InternalKeyKindSSTableInternalObsoleteBit if the key-value pair is obsolete + in the context of that sstable (for a reader that reads at a higher seqnum + than the highest seqnum in the sstable). For details, see the comment in + format.go. */ const ( @@ -316,12 +335,11 @@ func readFooter(f objstorage.Readable) (footer, error) { off := size - maxFooterLen if off < 0 { off = 0 + buf = buf[:size] } - n, err := f.ReadAt(buf, off) - if err != nil && err != io.EOF { + if err := f.ReadAt(context.TODO(), buf, off); err != nil { return footer, errors.Wrap(err, "pebble/table: invalid table (could not read footer)") } - buf = buf[:n] switch magic := buf[len(buf)-len(rocksDBMagic):]; string(magic) { case levelDBMagic: @@ -429,7 +447,7 @@ func supportsTwoLevelIndex(format TableFormat) bool { switch format { case TableFormatLevelDB: return false - case TableFormatRocksDBv2, TableFormatPebblev1, TableFormatPebblev2, TableFormatPebblev3: + case TableFormatRocksDBv2, TableFormatPebblev1, TableFormatPebblev2, TableFormatPebblev3, TableFormatPebblev4: return true default: panic("sstable: unspecified table format version") diff --git a/vendor/github.com/cockroachdb/pebble/sstable/value_block.go b/vendor/github.com/cockroachdb/pebble/sstable/value_block.go index cc033df..447348b 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/value_block.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/value_block.go @@ -13,17 +13,83 @@ import ( "github.com/cockroachdb/errors" "github.com/cockroachdb/pebble/internal/base" - "github.com/cockroachdb/pebble/internal/cache" "github.com/cockroachdb/pebble/internal/invariants" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" "golang.org/x/exp/rand" ) // Value blocks are supported in TableFormatPebblev3. // +// 1. Motivation and overview +// // Value blocks are a mechanism designed for sstables storing MVCC data, where // there can be many versions of a key that need to be kept, but only the -// latest value is typically read. See the documentation for Comparer.Split -// regarding MVCC keys. +// latest value is typically read (see the documentation for Comparer.Split +// regarding MVCC keys). The goal is faster reads. Unlike Pebble versions, +// which can be eagerly thrown away (except when there are snapshots), MVCC +// versions are long-lived (e.g. default CockroachDB garbage collection +// threshold for older versions is 24 hours) and can significantly slow down +// reads. We have seen CockroachDB production workloads with very slow reads +// due to: +// - 100s of versions for each key in a table. +// +// - Tables with mostly MVCC garbage consisting of 2 versions per key -- a +// real key-value pair, followed by a key-value pair whose value (usually +// with zero byte length) indicates it is an MVCC tombstone. +// +// The value blocks mechanism attempts to improve read throughput in these +// cases when the key size is smaller than the value sizes of older versions. +// This is done by moving the value of an older version to a value block in a +// different part of the sstable. This improves spatial locality of the data +// being read by the workload, which increases caching effectiveness. +// +// Additionally, even when the key size is not smaller than the value of older +// versions (e.g. secondary indexes in CockroachDB), TableFormatPebblev3 +// stores the result of key comparisons done at write time inside the sstable, +// which makes stepping from one key prefix to the next prefix (i.e., skipping +// over older versions of a MVCC key) more efficient by avoiding key +// comparisons and key decoding. See the results in +// https://github.com/cockroachdb/pebble/pull/2149 and more details in the +// comment inside BenchmarkIteratorScanNextPrefix. These improvements are also +// visible in end-to-end CockroachDB tests, as outlined in +// https://github.com/cockroachdb/cockroach/pull/96652. +// +// In TableFormatPebblev3, each SET has a one byte value prefix that tells us +// whether the value is in-place or in a value block. This 1 byte prefix +// encodes additional information: +// +// - ShortAttribute: This is an attribute of the value. Currently, CockroachDB +// uses it to represent whether the value is a tombstone or not. This avoids +// the need to fetch a value from the value block if the caller only wants +// to figure out whether it is an MVCC tombstone. The length of the value is +// another attribute that the caller can be interested in, and it is also +// accessible without reading the value in the value block (see the value +// handle in the details section). +// +// - SET-same-prefix: this enables the aforementioned optimization when +// stepping from one key prefix to the next key prefix. +// +// We further optimize this iteration over prefixes by using the restart +// points in a block to encode whether the SET at a restart point has the same +// prefix since the last restart point. This allows us to skip over restart +// points within the same block. See the comment in blockWriter, and how both +// SET-same-prefix and the restart point information is used in +// blockIter.nextPrefixV3. +// +// This flexibility of values that are in-place or in value blocks requires +// flexibility in the iterator interface. The InternalIterator interface +// returns a LazyValue instead of a byte slice. Additionally, pebble.Iterator +// allows the caller to ask for a LazyValue. See lazy_value.go for details, +// including the memory lifetime management. +// +// For historical discussions about this feature, see the issue +// https://github.com/cockroachdb/pebble/issues/1170 and the prototype in +// https://github.com/cockroachdb/pebble/pull/1443. +// +// The code in this file mainly covers value block and related encodings. We +// discuss these in the next section. +// +// 2. Details // // Note that the notion of the latest value is local to the sstable. It is // possible that that latest value has been deleted by a sstable in a higher @@ -34,7 +100,7 @@ import ( // read them. The code in this file is agnostic to the policy regarding what // should be stored in value blocks -- it allows even the latest MVCC version // to be stored in a value block. The policy decision in made in the -// sstable.Writer. See Writer.makeAddPointDecision. +// sstable.Writer. See Writer.makeAddPointDecisionV3. // // Data blocks contain two kinds of SET keys: those with in-place values and // those with a value handle. To distinguish these two cases we use a single @@ -661,7 +727,7 @@ func (ukb *UserKeyPrefixBound) IsEmpty() bool { type blockProviderWhenOpen interface { readBlockForVBR( ctx context.Context, h BlockHandle, stats *base.InternalIteratorStats, - ) (cache.Handle, error) + ) (bufferHandle, error) } type blockProviderWhenClosed struct { @@ -682,8 +748,12 @@ func (bpwc *blockProviderWhenClosed) close() { func (bpwc blockProviderWhenClosed) readBlockForVBR( ctx context.Context, h BlockHandle, stats *base.InternalIteratorStats, -) (cache.Handle, error) { - return bpwc.r.readBlock(ctx, h, nil, nil, stats) +) (bufferHandle, error) { + ctx = objiotracing.WithBlockType(ctx, objiotracing.ValueBlock) + // TODO(jackson,sumeer): Consider whether to use a buffer pool in this case. + // The bpwc is not allowed to outlive the iterator tree, so it cannot + // outlive the buffer pool. + return bpwc.r.readBlock(ctx, h, nil, nil, stats, nil /* buffer pool */) } // ReaderProvider supports the implementation of blockProviderWhenClosed. @@ -722,16 +792,16 @@ type valueBlockReader struct { // The value blocks index is lazily retrieved the first time the reader // needs to read a value that resides in a value block. vbiBlock []byte - vbiCache cache.Handle + vbiCache bufferHandle // When sequentially iterating through all key-value pairs, the cost of // repeatedly getting a block that is already in the cache and releasing the - // cache.Handle can be ~40% of the cpu overhead. So the reader remembers the + // bufferHandle can be ~40% of the cpu overhead. So the reader remembers the // last value block it retrieved, in case there is locality of access, and // this value block can be used for the next value retrieval. valueBlockNum uint32 valueBlock []byte valueBlockPtr unsafe.Pointer - valueCache cache.Handle + valueCache bufferHandle lazyFetcher base.LazyFetcher closed bool bufToMangle []byte @@ -765,12 +835,12 @@ func (r *valueBlockReader) close() { // we were to reopen this valueBlockReader and retrieve the same // Handle.value from the cache, we don't want to accidentally unref it when // attempting to unref the old handle. - r.vbiCache = cache.Handle{} + r.vbiCache = bufferHandle{} r.valueBlock = nil r.valueBlockPtr = nil r.valueCache.Release() // See comment above. - r.valueCache = cache.Handle{} + r.valueCache = bufferHandle{} r.closed = true // rp, vbih, stats remain valid, so that LazyFetcher.ValueFetcher can be // implemented. diff --git a/vendor/github.com/cockroachdb/pebble/sstable/writer.go b/vendor/github.com/cockroachdb/pebble/sstable/writer.go index 3f3827a..883ee7c 100644 --- a/vendor/github.com/cockroachdb/pebble/sstable/writer.go +++ b/vendor/github.com/cockroachdb/pebble/sstable/writer.go @@ -119,7 +119,7 @@ type Writer struct { // the cache, providing a defense in depth against bugs which cause cache // collisions. cacheID uint64 - fileNum base.FileNum + fileNum base.DiskFileNum // The following fields are copied from Options. blockSize int blockSizeThreshold int @@ -132,6 +132,8 @@ type Writer struct { separator Separator successor Successor tableFormat TableFormat + isStrictObsolete bool + writingToLowestLevel bool cache *cache.Cache restartInterval int checksumType ChecksumType @@ -166,6 +168,7 @@ type Writer struct { props Properties propCollectors []TablePropertyCollector blockPropCollectors []BlockPropertyCollector + obsoleteCollector obsoleteKeyBlockPropertyCollector blockPropsEncoder blockPropertiesEncoder // filter accumulates the filter block. If populated, the filter ingests // either the output of w.split (i.e. a prefix extractor) if w.split is not @@ -215,6 +218,8 @@ type pointKeyInfo struct { // prefixLen uses w.split, if not nil. Only computed when w.valueBlockWriter // is not nil. prefixLen int + // True iff the point was marked obsolete. + isObsolete bool } type coordinationState struct { @@ -673,7 +678,12 @@ func (w *Writer) Set(key, value []byte) error { if w.err != nil { return w.err } - return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindSet), value) + if w.isStrictObsolete { + return errors.Errorf("use AddWithForceObsolete") + } + // forceObsolete is false based on the assumption that no RANGEDELs in the + // sstable delete the added points. + return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindSet), value, false) } // Delete deletes the value for the given key. The sequence number is set to @@ -685,7 +695,12 @@ func (w *Writer) Delete(key []byte) error { if w.err != nil { return w.err } - return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindDelete), nil) + if w.isStrictObsolete { + return errors.Errorf("use AddWithForceObsolete") + } + // forceObsolete is false based on the assumption that no RANGEDELs in the + // sstable delete the added points. + return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindDelete), nil, false) } // DeleteRange deletes all of the keys (and values) in the range [start,end) @@ -711,7 +726,13 @@ func (w *Writer) Merge(key, value []byte) error { if w.err != nil { return w.err } - return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindMerge), value) + if w.isStrictObsolete { + return errors.Errorf("use AddWithForceObsolete") + } + // forceObsolete is false based on the assumption that no RANGEDELs in the + // sstable that delete the added points. If the user configured this writer + // to be strict-obsolete, addPoint will reject the addition of this MERGE. + return w.addPoint(base.MakeInternalKey(key, 0, InternalKeyKindMerge), value, false) } // Add adds a key/value pair to the table being written. For a given Writer, @@ -721,6 +742,24 @@ func (w *Writer) Merge(key, value []byte) error { // point entries. Additionally, range deletion tombstones must be fragmented // (i.e. by keyspan.Fragmenter). func (w *Writer) Add(key InternalKey, value []byte) error { + if w.isStrictObsolete { + return errors.Errorf("use AddWithForceObsolete") + } + return w.AddWithForceObsolete(key, value, false) +} + +// AddWithForceObsolete must be used when writing a strict-obsolete sstable. +// +// forceObsolete indicates whether the caller has determined that this key is +// obsolete even though it may be the latest point key for this userkey. This +// should be set to true for keys obsoleted by RANGEDELs, and is required for +// strict-obsolete sstables. +// +// Note that there are two properties, S1 and S2 (see comment in format.go) +// that strict-obsolete ssts must satisfy. S2, due to RANGEDELs, is solely the +// responsibility of the caller. S1 is solely the responsibility of the +// callee. +func (w *Writer) AddWithForceObsolete(key InternalKey, value []byte, forceObsolete bool) error { if w.err != nil { return w.err } @@ -735,7 +774,7 @@ func (w *Writer) Add(key InternalKey, value []byte) error { "pebble: range keys must be added via one of the RangeKey* functions") return w.err } - return w.addPoint(key, value) + return w.addPoint(key, value, forceObsolete) } func (w *Writer) makeAddPointDecisionV2(key InternalKey) error { @@ -745,12 +784,7 @@ func (w *Writer) makeAddPointDecisionV2(key InternalKey) error { return nil } if !w.disableKeyOrderChecks { - prevPointUserKey := w.dataBlockBuf.dataBlock.curKey - n := len(prevPointUserKey) - base.InternalTrailerLen - if n < 0 { - panic("corrupt key in blockWriter buffer") - } - prevPointUserKey = prevPointUserKey[:n:n] + prevPointUserKey := w.dataBlockBuf.dataBlock.getCurUserKey() cmpUser := w.compare(prevPointUserKey, key.UserKey) if cmpUser > 0 || (cmpUser == 0 && prevTrailer <= key.Trailer) { return errors.Errorf( @@ -762,9 +796,17 @@ func (w *Writer) makeAddPointDecisionV2(key InternalKey) error { return nil } +// REQUIRES: at least one point has been written to the Writer. +func (w *Writer) getLastPointUserKey() []byte { + if w.dataBlockBuf.dataBlock.nEntries == 0 { + panic(errors.AssertionFailedf("no point keys added to writer")) + } + return w.dataBlockBuf.dataBlock.getCurUserKey() +} + func (w *Writer) makeAddPointDecisionV3( key InternalKey, valueLen int, -) (setHasSamePrefix bool, writeToValueBlock bool, err error) { +) (setHasSamePrefix bool, writeToValueBlock bool, isObsolete bool, err error) { prevPointKeyInfo := w.lastPointKeyInfo w.lastPointKeyInfo.userKeyLen = len(key.UserKey) w.lastPointKeyInfo.prefixLen = w.lastPointKeyInfo.userKeyLen @@ -772,18 +814,16 @@ func (w *Writer) makeAddPointDecisionV3( w.lastPointKeyInfo.prefixLen = w.split(key.UserKey) } w.lastPointKeyInfo.trailer = key.Trailer - if w.dataBlockBuf.dataBlock.nEntries == 0 { - return false, false, nil + w.lastPointKeyInfo.isObsolete = false + if !w.meta.HasPointKeys { + return false, false, false, nil } - prevPointUserKey := w.dataBlockBuf.dataBlock.curKey - n := len(prevPointUserKey) - base.InternalTrailerLen - if n < 0 { - panic("corrupt key in blockWriter buffer") - } - prevPointUserKey = prevPointUserKey[:n:n] + keyKind := base.TrailerKind(key.Trailer) + prevPointUserKey := w.getLastPointUserKey() prevPointKey := InternalKey{UserKey: prevPointUserKey, Trailer: prevPointKeyInfo.trailer} - considerWriteToValueBlock := base.TrailerKind(prevPointKeyInfo.trailer) == InternalKeyKindSet && - base.TrailerKind(key.Trailer) == InternalKeyKindSet + prevKeyKind := base.TrailerKind(prevPointKeyInfo.trailer) + considerWriteToValueBlock := prevKeyKind == InternalKeyKindSet && + keyKind == InternalKeyKindSet if considerWriteToValueBlock && !w.requiredInPlaceValueBound.IsEmpty() { keyPrefix := key.UserKey[:w.lastPointKeyInfo.prefixLen] cmpUpper := w.compare( @@ -812,14 +852,63 @@ func (w *Writer) makeAddPointDecisionV3( } else { cmpUser = w.compare(prevPointUserKey, key.UserKey) } + // Ensure that no one adds a point key kind without considering the obsolete + // handling for that kind. + switch keyKind { + case InternalKeyKindSet, InternalKeyKindSetWithDelete, InternalKeyKindMerge, + InternalKeyKindDelete, InternalKeyKindSingleDelete, InternalKeyKindDeleteSized: + default: + panic(errors.AssertionFailedf("unexpected key kind %s", keyKind.String())) + } + // If same user key, then the current key is obsolete if any of the + // following is true: + // C1 The prev key was obsolete. + // C2 The prev key was not a MERGE. When the previous key is a MERGE we must + // preserve SET* and MERGE since their values will be merged into the + // previous key. We also must preserve DEL* since there may be an older + // SET*/MERGE in a lower level that must not be merged with the MERGE -- + // if we omit the DEL* that lower SET*/MERGE will become visible. + // + // Regardless of whether it is the same user key or not + // C3 The current key is some kind of point delete, and we are writing to + // the lowest level, then it is also obsolete. The correctness of this + // relies on the same user key not spanning multiple sstables in a level. + // + // C1 ensures that for a user key there is at most one transition from + // !obsolete to obsolete. Consider a user key k, for which the first n keys + // are not obsolete. We consider the various value of n: + // + // n = 0: This happens due to forceObsolete being set by the caller, or due + // to C3. forceObsolete must only be set due a RANGEDEL, and that RANGEDEL + // must also delete all the lower seqnums for the same user key. C3 triggers + // due to a point delete and that deletes all the lower seqnums for the same + // user key. + // + // n = 1: This is the common case. It happens when the first key is not a + // MERGE, or the current key is some kind of point delete. + // + // n > 1: This is due to a sequence of MERGE keys, potentially followed by a + // single non-MERGE key. + isObsoleteC1AndC2 := cmpUser == 0 && + (prevPointKeyInfo.isObsolete || prevKeyKind != InternalKeyKindMerge) + isObsoleteC3 := w.writingToLowestLevel && + (keyKind == InternalKeyKindDelete || keyKind == InternalKeyKindSingleDelete || + keyKind == InternalKeyKindDeleteSized) + isObsolete = isObsoleteC1AndC2 || isObsoleteC3 + // TODO(sumeer): storing isObsolete SET and SETWITHDEL in value blocks is + // possible, but requires some care in documenting and checking invariants. + // There is code that assumes nothing in value blocks because of single MVCC + // version (those should be ok). We have to ensure setHasSamePrefix is + // correctly initialized here etc. + if !w.disableKeyOrderChecks && (cmpUser > 0 || (cmpUser == 0 && prevPointKeyInfo.trailer <= key.Trailer)) { - return false, false, errors.Errorf( + return false, false, false, errors.Errorf( "pebble: keys must be added in strictly increasing order: %s, %s", prevPointKey.Pretty(w.formatKey), key.Pretty(w.formatKey)) } if !considerWriteToValueBlock { - return false, false, nil + return false, false, isObsolete, nil } // NB: it is possible that cmpUser == 0, i.e., these two SETs have identical // user keys (because of an open snapshot). This should be the rare case. @@ -832,19 +921,24 @@ func (w *Writer) makeAddPointDecisionV3( if considerWriteToValueBlock && valueLen <= tinyValueThreshold { considerWriteToValueBlock = false } - return setHasSamePrefix, considerWriteToValueBlock, nil + return setHasSamePrefix, considerWriteToValueBlock, isObsolete, nil } -func (w *Writer) addPoint(key InternalKey, value []byte) error { +func (w *Writer) addPoint(key InternalKey, value []byte, forceObsolete bool) error { + if w.isStrictObsolete && key.Kind() == InternalKeyKindMerge { + return errors.Errorf("MERGE not supported in a strict-obsolete sstable") + } var err error var setHasSameKeyPrefix, writeToValueBlock, addPrefixToValueStoredWithKey bool + var isObsolete bool maxSharedKeyLen := len(key.UserKey) if w.valueBlockWriter != nil { // maxSharedKeyLen is limited to the prefix of the preceding key. If the // preceding key was in a different block, then the blockWriter will // ignore this maxSharedKeyLen. maxSharedKeyLen = w.lastPointKeyInfo.prefixLen - setHasSameKeyPrefix, writeToValueBlock, err = w.makeAddPointDecisionV3(key, len(value)) + setHasSameKeyPrefix, writeToValueBlock, isObsolete, err = + w.makeAddPointDecisionV3(key, len(value)) addPrefixToValueStoredWithKey = base.TrailerKind(key.Trailer) == InternalKeyKindSet } else { err = w.makeAddPointDecisionV2(key) @@ -852,6 +946,8 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error { if err != nil { return err } + isObsolete = w.tableFormat >= TableFormatPebblev4 && (isObsolete || forceObsolete) + w.lastPointKeyInfo.isObsolete = isObsolete var valueStoredWithKey []byte var prefix valuePrefix var valueStoredWithKeyLen int @@ -907,16 +1003,19 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error { return err } } + if w.tableFormat >= TableFormatPebblev4 { + w.obsoleteCollector.AddPoint(isObsolete) + } w.maybeAddToFilter(key.UserKey) w.dataBlockBuf.dataBlock.addWithOptionalValuePrefix( - key, valueStoredWithKey, maxSharedKeyLen, addPrefixToValueStoredWithKey, prefix, + key, isObsolete, valueStoredWithKey, maxSharedKeyLen, addPrefixToValueStoredWithKey, prefix, setHasSameKeyPrefix) w.meta.updateSeqNum(key.SeqNum()) if !w.meta.HasPointKeys { - k := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey) + k := w.dataBlockBuf.dataBlock.getCurKey() // NB: We need to ensure that SmallestPoint.UserKey is set, so we create // an InternalKey which is semantically identical to the key, but won't // have a nil UserKey. We do this, because key.UserKey could be nil, and @@ -932,6 +1031,22 @@ func (w *Writer) addPoint(key InternalKey, value []byte) error { switch key.Kind() { case InternalKeyKindDelete, InternalKeyKindSingleDelete: w.props.NumDeletions++ + w.props.RawPointTombstoneKeySize += uint64(len(key.UserKey)) + case InternalKeyKindDeleteSized: + var size uint64 + if len(value) > 0 { + var n int + size, n = binary.Uvarint(value) + if n <= 0 { + w.err = errors.Newf("%s key's value (%x) does not parse as uvarint", + errors.Safe(key.Kind().String()), value) + return w.err + } + } + w.props.NumDeletions++ + w.props.NumSizedDeletions++ + w.props.RawPointTombstoneKeySize += uint64(len(key.UserKey)) + w.props.RawPointTombstoneValueSize += size case InternalKeyKindMerge: w.props.NumMergeOperands++ } @@ -952,7 +1067,7 @@ func (w *Writer) addTombstone(key InternalKey, value []byte) error { if !w.disableKeyOrderChecks && !w.rangeDelV1Format && w.rangeDelBlock.nEntries > 0 { // Check that tombstones are being added in fragmented order. If the two // tombstones overlap, their start and end keys must be identical. - prevKey := base.DecodeInternalKey(w.rangeDelBlock.curKey) + prevKey := w.rangeDelBlock.getCurKey() switch c := w.compare(prevKey.UserKey, key.UserKey); { case c > 0: w.err = errors.Errorf("pebble: keys must be added in order: %s, %s", @@ -1148,7 +1263,7 @@ func (w *Writer) encodeRangeKeySpan(span keyspan.Span) { func (w *Writer) addRangeKey(key InternalKey, value []byte) error { if !w.disableKeyOrderChecks && w.rangeKeyBlock.nEntries > 0 { - prevStartKey := base.DecodeInternalKey(w.rangeKeyBlock.curKey) + prevStartKey := w.rangeKeyBlock.getCurKey() prevEndKey, _, ok := rangekey.DecodeEndKey(prevStartKey.Kind(), w.rangeKeyBlock.curValue) if !ok { // We panic here as we should have previously decoded and validated this @@ -1294,7 +1409,7 @@ func (w *Writer) flush(key InternalKey) error { // dataBlockBuf is added back to a sync.Pool. In this particular case, the // byte slice which supports "sep" will eventually be copied when "sep" is // added to the index block. - prevKey := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey) + prevKey := w.dataBlockBuf.dataBlock.getCurKey() sep := w.indexEntrySep(prevKey, key, w.dataBlockBuf) // We determine that we should flush an index block from the Writer client // goroutine, but we actually finish the index block from the writeQueue. @@ -1584,7 +1699,7 @@ func (w *Writer) finishIndexBlock(indexBuf *indexBlockBuf, props []byte) error { nEntries: indexBuf.block.nEntries, properties: props, } w.indexSepAlloc, part.sep = cloneKeyWithBuf( - base.DecodeInternalKey(indexBuf.block.curKey), w.indexSepAlloc, + indexBuf.block.getCurKey(), w.indexSepAlloc, ) bk := indexBuf.finish() if len(w.indexBlockAlloc) < len(bk) { @@ -1660,7 +1775,7 @@ func compressAndChecksum(b []byte, compression Compression, blockBuf *blockBuf) func (w *Writer) writeCompressedBlock(block []byte, blockTrailerBuf []byte) (BlockHandle, error) { bh := BlockHandle{Offset: w.meta.Size, Length: uint64(len(block))} - if w.cacheID != 0 && w.fileNum != 0 { + if w.cacheID != 0 && w.fileNum.FileNum() != 0 { // Remove the block being written from the cache. This provides defense in // depth against bugs which cause cache collisions. // @@ -1687,7 +1802,7 @@ func (w *Writer) writeCompressedBlock(block []byte, blockTrailerBuf []byte) (Blo // return a BlockHandle. func (w *Writer) Write(blockWithTrailer []byte) (n int, err error) { offset := w.meta.Size - if w.cacheID != 0 && w.fileNum != 0 { + if w.cacheID != 0 && w.fileNum.FileNum() != 0 { // Remove the block being written from the cache. This provides defense in // depth against bugs which cause cache collisions. // @@ -1728,12 +1843,20 @@ func (w *Writer) assertFormatCompatibility() error { ) } + // PebbleDBv3: value blocks. if (w.props.NumValueBlocks > 0 || w.props.NumValuesInValueBlocks > 0 || w.props.ValueBlocksSize > 0) && w.tableFormat < TableFormatPebblev3 { return errors.Newf( "table format version %s is less than the minimum required version %s for value blocks", w.tableFormat, TableFormatPebblev3) } + + // PebbleDBv4: DELSIZED tombstones. + if w.props.NumSizedDeletions > 0 && w.tableFormat < TableFormatPebblev4 { + return errors.Newf( + "table format version %s is less than the minimum required version %s for sized deletion tombstones", + w.tableFormat, TableFormatPebblev4) + } return nil } @@ -1780,7 +1903,7 @@ func (w *Writer) Close() (err error) { // however, if a dataBlock is flushed, then we add a key to the new w.dataBlockBuf in the // addPoint function after the flush occurs. if w.dataBlockBuf.dataBlock.nEntries >= 1 { - w.meta.SetLargestPointKey(base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey).Clone()) + w.meta.SetLargestPointKey(w.dataBlockBuf.dataBlock.getCurKey().Clone()) } // Finish the last data block, or force an empty data block if there @@ -1794,7 +1917,7 @@ func (w *Writer) Close() (err error) { if err != nil { return err } - prevKey := base.DecodeInternalKey(w.dataBlockBuf.dataBlock.curKey) + prevKey := w.dataBlockBuf.dataBlock.getCurKey() if err := w.addIndexEntrySync(prevKey, InternalKey{}, bhp, w.dataBlockBuf.tmp[:]); err != nil { return err } @@ -1873,7 +1996,7 @@ func (w *Writer) Close() (err error) { var rangeKeyBH BlockHandle if w.props.NumRangeKeys() > 0 { - key := base.DecodeInternalKey(w.rangeKeyBlock.curKey) + key := w.rangeKeyBlock.getCurKey() kind := key.Kind() endKey, _, ok := rangekey.DecodeEndKey(kind, w.rangeKeyBlock.curValue) if !ok { @@ -1948,7 +2071,7 @@ func (w *Writer) Close() (err error) { // reduces table size without a significant impact on performance. raw.restartInterval = propertiesBlockRestartInterval w.props.CompressionOptions = rocksDBCompressionOptions - w.props.save(&raw) + w.props.save(w.tableFormat, &raw) bh, err := w.writeBlock(raw.finish(), NoCompression, &w.blockBuf) if err != nil { return err @@ -2062,7 +2185,7 @@ func (o PreviousPointKeyOpt) UnsafeKey() base.InternalKey { if o.w.dataBlockBuf.dataBlock.nEntries >= 1 { // o.w.dataBlockBuf.dataBlock.curKey is guaranteed to point to the last point key // which was added to the Writer. - return base.DecodeInternalKey(o.w.dataBlockBuf.dataBlock.curKey) + return o.w.dataBlockBuf.dataBlock.getCurKey() } return base.InternalKey{} } @@ -2091,6 +2214,8 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write separator: o.Comparer.Separator, successor: o.Comparer.Successor, tableFormat: o.TableFormat, + isStrictObsolete: o.IsStrictObsolete, + writingToLowestLevel: o.WritingToLowestLevel, cache: o.Cache, restartInterval: o.BlockRestartInterval, checksumType: o.Checksum, @@ -2109,7 +2234,7 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write Format: o.Comparer.FormatKey, }, } - if w.tableFormat == TableFormatPebblev3 { + if w.tableFormat >= TableFormatPebblev3 { w.shortAttributeExtractor = o.ShortAttributeExtractor w.requiredInPlaceValueBound = o.RequiredInPlaceValueBound w.valueBlockWriter = newValueBlockWriter( @@ -2157,14 +2282,14 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write } } - w.props.ColumnFamilyID = math.MaxInt32 w.props.ComparerName = o.Comparer.Name w.props.CompressionName = o.Compression.String() w.props.MergerName = o.MergerName w.props.PropertyCollectorNames = "[]" w.props.ExternalFormatVersion = rocksDBExternalFormatVersion - if len(o.TablePropertyCollectors) > 0 || len(o.BlockPropertyCollectors) > 0 { + if len(o.TablePropertyCollectors) > 0 || len(o.BlockPropertyCollectors) > 0 || + w.tableFormat >= TableFormatPebblev4 { var buf bytes.Buffer buf.WriteString("[") if len(o.TablePropertyCollectors) > 0 { @@ -2177,16 +2302,22 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write buf.WriteString(w.propCollectors[i].Name()) } } + numBlockPropertyCollectors := len(o.BlockPropertyCollectors) + if w.tableFormat >= TableFormatPebblev4 { + numBlockPropertyCollectors++ + } + // shortID is a uint8, so we cannot exceed that number of block + // property collectors. + if numBlockPropertyCollectors > math.MaxUint8 { + w.err = errors.New("pebble: too many block property collectors") + return w + } + if numBlockPropertyCollectors > 0 { + w.blockPropCollectors = make([]BlockPropertyCollector, numBlockPropertyCollectors) + } if len(o.BlockPropertyCollectors) > 0 { - // shortID is a uint8, so we cannot exceed that number of block - // property collectors. - if len(o.BlockPropertyCollectors) > math.MaxUint8 { - w.err = errors.New("pebble: too many block property collectors") - return w - } // The shortID assigned to a collector is the same as its index in // this slice. - w.blockPropCollectors = make([]BlockPropertyCollector, len(o.BlockPropertyCollectors)) for i := range o.BlockPropertyCollectors { w.blockPropCollectors[i] = o.BlockPropertyCollectors[i]() if i > 0 || len(o.TablePropertyCollectors) > 0 { @@ -2195,6 +2326,13 @@ func NewWriter(writable objstorage.Writable, o WriterOptions, extraOpts ...Write buf.WriteString(w.blockPropCollectors[i].Name()) } } + if w.tableFormat >= TableFormatPebblev4 { + if numBlockPropertyCollectors > 1 || len(o.TablePropertyCollectors) > 0 { + buf.WriteString(",") + } + w.blockPropCollectors[numBlockPropertyCollectors-1] = &w.obsoleteCollector + buf.WriteString(w.obsoleteCollector.Name()) + } buf.WriteString("]") w.props.PropertyCollectorNames = buf.String() } @@ -2228,3 +2366,85 @@ func init() { } private.SSTableInternalProperties = internalGetProperties } + +type obsoleteKeyBlockPropertyCollector struct { + blockIsNonObsolete bool + indexIsNonObsolete bool + tableIsNonObsolete bool +} + +func encodeNonObsolete(isNonObsolete bool, buf []byte) []byte { + if isNonObsolete { + return buf + } + return append(buf, 't') +} + +func (o *obsoleteKeyBlockPropertyCollector) Name() string { + return "obsolete-key" +} + +func (o *obsoleteKeyBlockPropertyCollector) Add(key InternalKey, value []byte) error { + // Ignore. + return nil +} + +func (o *obsoleteKeyBlockPropertyCollector) AddPoint(isObsolete bool) { + o.blockIsNonObsolete = o.blockIsNonObsolete || !isObsolete +} + +func (o *obsoleteKeyBlockPropertyCollector) FinishDataBlock(buf []byte) ([]byte, error) { + o.tableIsNonObsolete = o.tableIsNonObsolete || o.blockIsNonObsolete + return encodeNonObsolete(o.blockIsNonObsolete, buf), nil +} + +func (o *obsoleteKeyBlockPropertyCollector) AddPrevDataBlockToIndexBlock() { + o.indexIsNonObsolete = o.indexIsNonObsolete || o.blockIsNonObsolete + o.blockIsNonObsolete = false +} + +func (o *obsoleteKeyBlockPropertyCollector) FinishIndexBlock(buf []byte) ([]byte, error) { + indexIsNonObsolete := o.indexIsNonObsolete + o.indexIsNonObsolete = false + return encodeNonObsolete(indexIsNonObsolete, buf), nil +} + +func (o *obsoleteKeyBlockPropertyCollector) FinishTable(buf []byte) ([]byte, error) { + return encodeNonObsolete(o.tableIsNonObsolete, buf), nil +} + +func (o *obsoleteKeyBlockPropertyCollector) UpdateKeySuffixes( + oldProp []byte, oldSuffix, newSuffix []byte, +) error { + _, err := propToIsObsolete(oldProp) + if err != nil { + return err + } + // Suffix rewriting currently loses the obsolete bit. + o.blockIsNonObsolete = true + return nil +} + +// NB: obsoleteKeyBlockPropertyFilter is stateless. This aspect of the filter +// is used in table_cache.go for in-place modification of a filters slice. +type obsoleteKeyBlockPropertyFilter struct{} + +func (o obsoleteKeyBlockPropertyFilter) Name() string { + return "obsolete-key" +} + +// Intersects returns true if the set represented by prop intersects with +// the set in the filter. +func (o obsoleteKeyBlockPropertyFilter) Intersects(prop []byte) (bool, error) { + return propToIsObsolete(prop) +} + +func propToIsObsolete(prop []byte) (bool, error) { + if len(prop) == 0 { + return true, nil + } + if len(prop) > 1 || prop[0] != 't' { + return false, errors.Errorf("unexpected property %x", prop) + } + return false, nil +} diff --git a/vendor/github.com/cockroachdb/pebble/table_cache.go b/vendor/github.com/cockroachdb/pebble/table_cache.go index 9add2b3..516e5e8 100644 --- a/vendor/github.com/cockroachdb/pebble/table_cache.go +++ b/vendor/github.com/cockroachdb/pebble/table_cache.go @@ -22,6 +22,7 @@ import ( "github.com/cockroachdb/pebble/internal/manifest" "github.com/cockroachdb/pebble/internal/private" "github.com/cockroachdb/pebble/objstorage" + "github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing" "github.com/cockroachdb/pebble/sstable" ) @@ -58,18 +59,15 @@ var tableCacheLabels = pprof.Labels("pebble", "table-cache") // are updated, we could have unnecessary evictions of those // fields, and the surrounding fields from the CPU caches. type tableCacheOpts struct { - atomic struct { - // iterCount in the tableCacheOpts keeps track of iterators - // opened or closed by a DB. It's used to keep track of - // leaked iterators on a per-db level. - iterCount *int32 - } + // iterCount keeps track of how many iterators are open. It is used to keep + // track of leaked iterators on a per-db level. + iterCount *atomic.Int32 loggerAndTracer LoggerAndTracer cacheID uint64 - objProvider *objstorage.Provider + objProvider objstorage.Provider opts sstable.ReaderOptions - filterMetrics *FilterMetrics + filterMetrics *sstable.FilterMetricsTracker } // tableCacheContainer contains the table cache and @@ -85,7 +83,7 @@ type tableCacheContainer struct { // newTableCacheContainer will panic if the underlying cache in the table cache // doesn't match Options.Cache. func newTableCacheContainer( - tc *TableCache, cacheID uint64, objProvider *objstorage.Provider, opts *Options, size int, + tc *TableCache, cacheID uint64, objProvider objstorage.Provider, opts *Options, size int, ) *tableCacheContainer { // We will release a ref to table cache acquired here when tableCacheContainer.close is called. if tc != nil { @@ -105,8 +103,8 @@ func newTableCacheContainer( t.dbOpts.cacheID = cacheID t.dbOpts.objProvider = objProvider t.dbOpts.opts = opts.MakeReaderOptions() - t.dbOpts.filterMetrics = &FilterMetrics{} - t.dbOpts.atomic.iterCount = new(int32) + t.dbOpts.filterMetrics = &sstable.FilterMetricsTracker{} + t.dbOpts.iterCount = new(atomic.Int32) return t } @@ -117,7 +115,7 @@ func (c *tableCacheContainer) close() error { // by the DB using this container. Note that we'll still perform cleanup // below in the case that there are leaked iterators. var err error - if v := atomic.LoadInt32(c.dbOpts.atomic.iterCount); v > 0 { + if v := c.dbOpts.iterCount.Load(); v > 0 { err = errors.Errorf("leaked iterators: %d", errors.Safe(v)) } @@ -136,20 +134,22 @@ func (c *tableCacheContainer) newIters( opts *IterOptions, internalOpts internalIterOpts, ) (internalIterator, keyspan.FragmentIterator, error) { - return c.tableCache.getShard(file.FileNum).newIters(ctx, file, opts, internalOpts, &c.dbOpts) + return c.tableCache.getShard(file.FileBacking.DiskFileNum).newIters(ctx, file, opts, internalOpts, &c.dbOpts) } func (c *tableCacheContainer) newRangeKeyIter( - file *manifest.FileMetadata, opts *keyspan.SpanIterOptions, + file *manifest.FileMetadata, opts keyspan.SpanIterOptions, ) (keyspan.FragmentIterator, error) { - return c.tableCache.getShard(file.FileNum).newRangeKeyIter(file, opts, &c.dbOpts) + return c.tableCache.getShard(file.FileBacking.DiskFileNum).newRangeKeyIter(file, opts, &c.dbOpts) } +// getTableProperties returns the properties associated with the backing physical +// table if the input metadata belongs to a virtual sstable. func (c *tableCacheContainer) getTableProperties(file *fileMetadata) (*sstable.Properties, error) { - return c.tableCache.getShard(file.FileNum).getTableProperties(file, &c.dbOpts) + return c.tableCache.getShard(file.FileBacking.DiskFileNum).getTableProperties(file, &c.dbOpts) } -func (c *tableCacheContainer) evict(fileNum FileNum) { +func (c *tableCacheContainer) evict(fileNum base.DiskFileNum) { c.tableCache.getShard(fileNum).evict(fileNum, &c.dbOpts, false) } @@ -160,41 +160,109 @@ func (c *tableCacheContainer) metrics() (CacheMetrics, FilterMetrics) { s.mu.RLock() m.Count += int64(len(s.mu.nodes)) s.mu.RUnlock() - m.Hits += atomic.LoadInt64(&s.atomic.hits) - m.Misses += atomic.LoadInt64(&s.atomic.misses) + m.Hits += s.hits.Load() + m.Misses += s.misses.Load() } m.Size = m.Count * int64(unsafe.Sizeof(sstable.Reader{})) - f := FilterMetrics{ - Hits: atomic.LoadInt64(&c.dbOpts.filterMetrics.Hits), - Misses: atomic.LoadInt64(&c.dbOpts.filterMetrics.Misses), - } + f := c.dbOpts.filterMetrics.Load() return m, f } -func (c *tableCacheContainer) withReader(meta *fileMetadata, fn func(*sstable.Reader) error) error { - s := c.tableCache.getShard(meta.FileNum) +func (c *tableCacheContainer) estimateSize( + meta *fileMetadata, lower, upper []byte, +) (size uint64, err error) { + if meta.Virtual { + err = c.withVirtualReader( + meta.VirtualMeta(), + func(r sstable.VirtualReader) (err error) { + size, err = r.EstimateDiskUsage(lower, upper) + return err + }, + ) + } else { + err = c.withReader( + meta.PhysicalMeta(), + func(r *sstable.Reader) (err error) { + size, err = r.EstimateDiskUsage(lower, upper) + return err + }, + ) + } + if err != nil { + return 0, err + } + return size, nil +} + +// createCommonReader creates a Reader for this file. isForeign, if true for +// virtual sstables, is passed into the vSSTable reader so its iterators can +// collapse obsolete points accordingly. +func createCommonReader( + v *tableCacheValue, file *fileMetadata, isForeign bool, +) sstable.CommonReader { + // TODO(bananabrick): We suffer an allocation if file is a virtual sstable. + var cr sstable.CommonReader = v.reader + if file.Virtual { + virtualReader := sstable.MakeVirtualReader( + v.reader, file.VirtualMeta(), isForeign, + ) + cr = &virtualReader + } + return cr +} + +func (c *tableCacheContainer) withCommonReader( + meta *fileMetadata, fn func(sstable.CommonReader) error, +) error { + s := c.tableCache.getShard(meta.FileBacking.DiskFileNum) v := s.findNode(meta, &c.dbOpts) defer s.unrefValue(v) if v.err != nil { return v.err } + provider := c.dbOpts.objProvider + objMeta, err := provider.Lookup(fileTypeTable, meta.FileBacking.DiskFileNum) + if err != nil { + return err + } + return fn(createCommonReader(v, meta, provider.IsSharedForeign(objMeta))) +} + +func (c *tableCacheContainer) withReader(meta physicalMeta, fn func(*sstable.Reader) error) error { + s := c.tableCache.getShard(meta.FileBacking.DiskFileNum) + v := s.findNode(meta.FileMetadata, &c.dbOpts) + defer s.unrefValue(v) + if v.err != nil { + return v.err + } return fn(v.reader) } +// withVirtualReader fetches a VirtualReader associated with a virtual sstable. +func (c *tableCacheContainer) withVirtualReader( + meta virtualMeta, fn func(sstable.VirtualReader) error, +) error { + s := c.tableCache.getShard(meta.FileBacking.DiskFileNum) + v := s.findNode(meta.FileMetadata, &c.dbOpts) + defer s.unrefValue(v) + if v.err != nil { + return v.err + } + provider := c.dbOpts.objProvider + objMeta, err := provider.Lookup(fileTypeTable, meta.FileBacking.DiskFileNum) + if err != nil { + return err + } + return fn(sstable.MakeVirtualReader(v.reader, meta, provider.IsSharedForeign(objMeta))) +} + func (c *tableCacheContainer) iterCount() int64 { - return int64(atomic.LoadInt32(c.dbOpts.atomic.iterCount)) + return int64(c.dbOpts.iterCount.Load()) } // TableCache is a shareable cache for open sstables. type TableCache struct { - // atomic contains fields which are accessed atomically. Go allocations - // are guaranteed to be 64-bit aligned which we take advantage of by - // placing the 64-bit fields which we access atomically at the beginning - // of the TableCache struct. For more information, see - // https://golang.org/pkg/sync/atomic/#pkg-note-BUG. - atomic struct { - refs int64 - } + refs atomic.Int64 cache *Cache shards []*tableCacheShard @@ -204,7 +272,7 @@ type TableCache struct { // the table cache only remains valid if there is at least one reference // to it. func (c *TableCache) Ref() { - v := atomic.AddInt64(&c.atomic.refs, 1) + v := c.refs.Add(1) // We don't want the reference count to ever go from 0 -> 1, // cause a reference count of 0 implies that we've closed the cache. if v <= 1 { @@ -214,7 +282,7 @@ func (c *TableCache) Ref() { // Unref removes a reference to the table cache. func (c *TableCache) Unref() error { - v := atomic.AddInt64(&c.atomic.refs, -1) + v := c.refs.Add(-1) switch { case v < 0: panic(fmt.Sprintf("pebble: inconsistent reference count: %d", v)) @@ -256,31 +324,24 @@ func NewTableCache(cache *Cache, numShards int, size int) *TableCache { } // Hold a ref to the cache here. - c.atomic.refs = 1 + c.refs.Store(1) return c } -func (c *TableCache) getShard(fileNum FileNum) *tableCacheShard { - return c.shards[uint64(fileNum)%uint64(len(c.shards))] +func (c *TableCache) getShard(fileNum base.DiskFileNum) *tableCacheShard { + return c.shards[uint64(fileNum.FileNum())%uint64(len(c.shards))] } type tableCacheKey struct { cacheID uint64 - fileNum FileNum + fileNum base.DiskFileNum } type tableCacheShard struct { - // WARNING: The following struct `atomic` contains fields are accessed atomically. - // - // Go allocations are guaranteed to be 64-bit aligned which we take advantage - // of by placing the 64-bit fields which we access atomically at the beginning - // of the DB struct. For more information, see https://golang.org/pkg/sync/atomic/#pkg-note-BUG. - atomic struct { - hits int64 - misses int64 - iterCount int32 - } + hits atomic.Int64 + misses atomic.Int64 + iterCount atomic.Int32 size int @@ -377,21 +438,52 @@ func (c *tableCacheShard) newIters( return nil, nil, v.err } + hideObsoletePoints := false + var pointKeyFilters []BlockPropertyFilter + if opts != nil { + // This code is appending (at most one filter) in-place to + // opts.PointKeyFilters even though the slice is shared for iterators in + // the same iterator tree. This is acceptable since all the following + // properties are true: + // - The iterator tree is single threaded, so the shared backing for the + // slice is being mutated in a single threaded manner. + // - Each shallow copy of the slice has its own notion of length. + // - The appended element is always the obsoleteKeyBlockPropertyFilter + // struct, which is stateless, so overwriting that struct when creating + // one sstable iterator is harmless to other sstable iterators that are + // relying on that struct. + // + // An alternative would be to have different slices for different sstable + // iterators, but that requires more work to avoid allocations. + hideObsoletePoints, pointKeyFilters = + v.reader.TryAddBlockPropertyFilterForHideObsoletePoints( + opts.snapshotForHideObsoletePoints, file.LargestSeqNum, opts.PointKeyFilters) + } ok := true var filterer *sstable.BlockPropertiesFilterer var err error if opts != nil { ok, filterer, err = c.checkAndIntersectFilters(v, opts.TableFilter, - opts.PointKeyFilters, internalOpts.boundLimitedFilter) + pointKeyFilters, internalOpts.boundLimitedFilter) } if err != nil { c.unrefValue(v) return nil, nil, err } + provider := dbOpts.objProvider + // Check if this file is a foreign file. + objMeta, err := provider.Lookup(fileTypeTable, file.FileBacking.DiskFileNum) + if err != nil { + return nil, nil, err + } + + // Note: This suffers an allocation for virtual sstables. + cr := createCommonReader(v, file, provider.IsSharedForeign(objMeta)) + // NB: range-del iterator does not maintain a reference to the table, nor // does it need to read from it after creation. - rangeDelIter, err := v.reader.NewRawRangeDelIter() + rangeDelIter, err := cr.NewRawRangeDelIter() if err != nil { c.unrefValue(v) return nil, nil, err @@ -417,20 +509,29 @@ func (c *tableCacheShard) newIters( useFilter := true if opts != nil { useFilter = manifest.LevelToInt(opts.level) != 6 || opts.UseL6Filters + ctx = objiotracing.WithLevel(ctx, manifest.LevelToInt(opts.level)) } tableFormat, err := v.reader.TableFormat() if err != nil { return nil, nil, err } var rp sstable.ReaderProvider - if tableFormat == sstable.TableFormatPebblev3 && v.reader.Properties.NumValueBlocks > 0 { + if tableFormat >= sstable.TableFormatPebblev3 && v.reader.Properties.NumValueBlocks > 0 { rp = &tableCacheShardReaderProvider{c: c, file: file, dbOpts: dbOpts} } + + if provider.IsSharedForeign(objMeta) { + if tableFormat < sstable.TableFormatPebblev4 { + return nil, nil, errors.New("pebble: shared foreign sstable has a lower table format than expected") + } + hideObsoletePoints = true + } if internalOpts.bytesIterated != nil { - iter, err = v.reader.NewCompactionIter(internalOpts.bytesIterated, rp) + iter, err = cr.NewCompactionIter(internalOpts.bytesIterated, rp, internalOpts.bufferPool) } else { - iter, err = v.reader.NewIterWithBlockPropertyFiltersAndContext( - ctx, opts.GetLowerBound(), opts.GetUpperBound(), filterer, useFilter, internalOpts.stats, rp) + iter, err = cr.NewIterWithBlockPropertyFiltersAndContextEtc( + ctx, opts.GetLowerBound(), opts.GetUpperBound(), filterer, hideObsoletePoints, useFilter, + internalOpts.stats, rp) } if err != nil { if rangeDelIter != nil { @@ -443,8 +544,8 @@ func (c *tableCacheShard) newIters( // care to avoid introducing an allocation here by adding a closure. iter.SetCloseHook(v.closeHook) - atomic.AddInt32(&c.atomic.iterCount, 1) - atomic.AddInt32(dbOpts.atomic.iterCount, 1) + c.iterCount.Add(1) + dbOpts.iterCount.Add(1) if invariants.RaceEnabled { c.mu.Lock() c.mu.iters[iter] = debug.Stack() @@ -454,7 +555,7 @@ func (c *tableCacheShard) newIters( } func (c *tableCacheShard) newRangeKeyIter( - file *manifest.FileMetadata, opts *keyspan.SpanIterOptions, dbOpts *tableCacheOpts, + file *manifest.FileMetadata, opts keyspan.SpanIterOptions, dbOpts *tableCacheOpts, ) (keyspan.FragmentIterator, error) { // Calling findNode gives us the responsibility of decrementing v's // refCount. If opening the underlying table resulted in error, then we @@ -473,7 +574,7 @@ func (c *tableCacheShard) newRangeKeyIter( // file's range key blocks may surface deleted range keys below. This is // done here, rather than deferring to the block-property collector in order // to maintain parity with point keys and the treatment of RANGEDELs. - if opts != nil && v.reader.Properties.NumRangeKeyDels == 0 { + if v.reader.Properties.NumRangeKeyDels == 0 { ok, _, err = c.checkAndIntersectFilters(v, nil, opts.RangeKeyFilters, nil) } if err != nil { @@ -488,7 +589,20 @@ func (c *tableCacheShard) newRangeKeyIter( } var iter keyspan.FragmentIterator - iter, err = v.reader.NewRawRangeKeyIter() + if file.Virtual { + provider := dbOpts.objProvider + var objMeta objstorage.ObjectMetadata + objMeta, err = provider.Lookup(fileTypeTable, file.FileBacking.DiskFileNum) + if err == nil { + virtualReader := sstable.MakeVirtualReader( + v.reader, file.VirtualMeta(), provider.IsSharedForeign(objMeta), + ) + iter, err = virtualReader.NewRawRangeKeyIter() + } + } else { + iter, err = v.reader.NewRawRangeKeyIter() + } + // iter is a block iter that holds the entire value of the block in memory. // No need to hold onto a ref of the cache value. c.unrefValue(v) @@ -522,6 +636,13 @@ var _ sstable.ReaderProvider = &tableCacheShardReaderProvider{} // being deleted. // // The caller must call tableCacheShardReaderProvider.Close. +// +// Note that currently the Reader returned here is only used to read value +// blocks. This reader shouldn't be used for other purposes like reading keys +// outside of virtual sstable bounds. +// +// TODO(bananabrick): We could return a wrapper over the Reader to ensure +// that the reader isn't used for other purposes. func (rp *tableCacheShardReaderProvider) GetReader() (*sstable.Reader, error) { // Calling findNode gives us the responsibility of decrementing v's // refCount. @@ -567,7 +688,7 @@ func (c *tableCacheShard) releaseNode(n *tableCacheNode) { // // c.mu must be held when calling this. func (c *tableCacheShard) unlinkNode(n *tableCacheNode) { - key := tableCacheKey{n.cacheID, n.meta.FileNum} + key := tableCacheKey{n.cacheID, n.fileNum} delete(c.mu.nodes, key) switch n.ptype { @@ -612,7 +733,7 @@ func (c *tableCacheShard) clearNode(n *tableCacheNode) { // it is present in tableCacheShard.mu.nodes, so a reference count of 0 means // the node has already been removed from that map. func (c *tableCacheShard) unrefValue(v *tableCacheValue) { - if atomic.AddInt32(&v.refCount, -1) == 0 { + if v.refCount.Add(-1) == 0 { c.releasing.Add(1) c.releasingCh <- v } @@ -622,18 +743,41 @@ func (c *tableCacheShard) unrefValue(v *tableCacheValue) { // that node if it didn't already exist. The caller is responsible for // decrementing the returned node's refCount. func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) *tableCacheValue { + v := c.findNodeInternal(meta, dbOpts) + + // Loading a file before its global sequence number is known (eg, + // during ingest before entering the commit pipeline) can pollute + // the cache with incorrect state. In invariant builds, verify + // that the global sequence number of the returned reader matches. + if invariants.Enabled { + if v.reader != nil && meta.LargestSeqNum == meta.SmallestSeqNum && + v.reader.Properties.GlobalSeqNum != meta.SmallestSeqNum { + panic(errors.AssertionFailedf("file %s loaded from table cache with the wrong global sequence number %d", + meta, v.reader.Properties.GlobalSeqNum)) + } + } + return v +} + +func (c *tableCacheShard) findNodeInternal( + meta *fileMetadata, dbOpts *tableCacheOpts, +) *tableCacheValue { + if refs := meta.Refs(); refs <= 0 { + panic(errors.AssertionFailedf("attempting to load file %s with refs=%d from table cache", + meta, refs)) + } // Fast-path for a hit in the cache. c.mu.RLock() - key := tableCacheKey{dbOpts.cacheID, meta.FileNum} + key := tableCacheKey{dbOpts.cacheID, meta.FileBacking.DiskFileNum} if n := c.mu.nodes[key]; n != nil && n.value != nil { // Fast-path hit. // // The caller is responsible for decrementing the refCount. v := n.value - atomic.AddInt32(&v.refCount, 1) + v.refCount.Add(1) c.mu.RUnlock() - atomic.StoreInt32(&n.referenced, 1) - atomic.AddInt64(&c.atomic.hits, 1) + n.referenced.Store(true) + c.hits.Add(1) <-v.loaded return v } @@ -646,8 +790,8 @@ func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) * case n == nil: // Slow-path miss of a non-existent node. n = &tableCacheNode{ - meta: meta, - ptype: tableCacheNodeCold, + fileNum: meta.FileBacking.DiskFileNum, + ptype: tableCacheNodeCold, } c.addNode(n, dbOpts) c.mu.sizeCold++ @@ -657,9 +801,9 @@ func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) * // // The caller is responsible for decrementing the refCount. v := n.value - atomic.AddInt32(&v.refCount, 1) - atomic.StoreInt32(&n.referenced, 1) - atomic.AddInt64(&c.atomic.hits, 1) + v.refCount.Add(1) + n.referenced.Store(true) + c.hits.Add(1) c.mu.Unlock() <-v.loaded return v @@ -672,18 +816,18 @@ func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) * c.mu.coldTarget = c.size } - atomic.StoreInt32(&n.referenced, 0) + n.referenced.Store(false) n.ptype = tableCacheNodeHot c.addNode(n, dbOpts) c.mu.sizeHot++ } - atomic.AddInt64(&c.atomic.misses, 1) + c.misses.Add(1) v := &tableCacheValue{ - loaded: make(chan struct{}), - refCount: 2, + loaded: make(chan struct{}), } + v.refCount.Store(2) // Cache the closure invoked when an iterator is closed. This avoids an // allocation on every call to newIters. v.closeHook = func(i sstable.Iterator) error { @@ -693,8 +837,8 @@ func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) * c.mu.Unlock() } c.unrefValue(v) - atomic.AddInt32(&c.atomic.iterCount, -1) - atomic.AddInt32(dbOpts.atomic.iterCount, -1) + c.iterCount.Add(-1) + dbOpts.iterCount.Add(-1) return nil } n.value = v @@ -704,7 +848,12 @@ func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) * // Note adding to the cache lists must complete before we begin loading the // table as a failure during load will result in the node being unlinked. pprof.Do(context.Background(), tableCacheLabels, func(context.Context) { - v.load(meta, c, dbOpts) + v.load( + loadInfo{ + backingFileNum: meta.FileBacking.DiskFileNum, + smallestSeqNum: meta.SmallestSeqNum, + largestSeqNum: meta.LargestSeqNum, + }, c, dbOpts) }) return v } @@ -712,7 +861,7 @@ func (c *tableCacheShard) findNode(meta *fileMetadata, dbOpts *tableCacheOpts) * func (c *tableCacheShard) addNode(n *tableCacheNode, dbOpts *tableCacheOpts) { c.evictNodes() n.cacheID = dbOpts.cacheID - key := tableCacheKey{n.cacheID, n.meta.FileNum} + key := tableCacheKey{n.cacheID, n.fileNum} c.mu.nodes[key] = n n.links.next = n @@ -740,8 +889,8 @@ func (c *tableCacheShard) evictNodes() { func (c *tableCacheShard) runHandCold() { n := c.mu.handCold if n.ptype == tableCacheNodeCold { - if atomic.LoadInt32(&n.referenced) == 1 { - atomic.StoreInt32(&n.referenced, 0) + if n.referenced.Load() { + n.referenced.Store(false) n.ptype = tableCacheNodeHot c.mu.sizeCold-- c.mu.sizeHot++ @@ -773,8 +922,8 @@ func (c *tableCacheShard) runHandHot() { n := c.mu.handHot if n.ptype == tableCacheNodeHot { - if atomic.LoadInt32(&n.referenced) == 1 { - atomic.StoreInt32(&n.referenced, 0) + if n.referenced.Load() { + n.referenced.Store(false) } else { n.ptype = tableCacheNodeCold c.mu.sizeHot-- @@ -806,7 +955,7 @@ func (c *tableCacheShard) runHandTest() { c.mu.handTest = c.mu.handTest.next() } -func (c *tableCacheShard) evict(fileNum FileNum, dbOpts *tableCacheOpts, allowLeak bool) { +func (c *tableCacheShard) evict(fileNum base.DiskFileNum, dbOpts *tableCacheOpts, allowLeak bool) { c.mu.Lock() key := tableCacheKey{dbOpts.cacheID, fileNum} n := c.mu.nodes[key] @@ -821,7 +970,7 @@ func (c *tableCacheShard) evict(fileNum FileNum, dbOpts *tableCacheOpts, allowLe v = n.value if v != nil { if !allowLeak { - if t := atomic.AddInt32(&v.refCount, -1); t != 0 { + if t := v.refCount.Add(-1); t != 0 { dbOpts.loggerAndTracer.Fatalf("sstable %s: refcount is not zero: %d\n%s", fileNum, t, debug.Stack()) } } @@ -842,7 +991,7 @@ func (c *tableCacheShard) evict(fileNum FileNum, dbOpts *tableCacheOpts, allowLe // associated with dbOpts.cacheID. Make sure that there will // be no more accesses to the files associated with the DB. func (c *tableCacheShard) removeDB(dbOpts *tableCacheOpts) { - var fileNums []base.FileNum + var fileNums []base.DiskFileNum c.mu.RLock() // Collect the fileNums which need to be cleaned. @@ -854,7 +1003,7 @@ func (c *tableCacheShard) removeDB(dbOpts *tableCacheOpts) { } if node.cacheID == dbOpts.cacheID { - fileNums = append(fileNums, node.meta.FileNum) + fileNums = append(fileNums, node.fileNum) } node = node.next() } @@ -863,8 +1012,8 @@ func (c *tableCacheShard) removeDB(dbOpts *tableCacheOpts) { // Evict all the nodes associated with the DB. // This should synchronously close all the files // associated with the DB. - for _, fNum := range fileNums { - c.evict(fNum, dbOpts, true) + for _, fileNum := range fileNums { + c.evict(fileNum, dbOpts, true) } } @@ -875,7 +1024,7 @@ func (c *tableCacheShard) Close() error { // Check for leaked iterators. Note that we'll still perform cleanup below in // the case that there are leaked iterators. var err error - if v := atomic.LoadInt32(&c.atomic.iterCount); v > 0 { + if v := c.iterCount.Load(); v > 0 { if !invariants.RaceEnabled { err = errors.Errorf("leaked iterators: %d", errors.Safe(v)) } else { @@ -890,7 +1039,7 @@ func (c *tableCacheShard) Close() error { for c.mu.handHot != nil { n := c.mu.handHot if n.value != nil { - if atomic.AddInt32(&n.value.refCount, -1) == 0 { + if n.value.refCount.Add(-1) == 0 { c.releasing.Add(1) c.releasingCh <- n.value } @@ -926,31 +1075,39 @@ type tableCacheValue struct { loaded chan struct{} // Reference count for the value. The reader is closed when the reference // count drops to zero. - refCount int32 + refCount atomic.Int32 } -func (v *tableCacheValue) load(meta *fileMetadata, c *tableCacheShard, dbOpts *tableCacheOpts) { +type loadInfo struct { + backingFileNum base.DiskFileNum + largestSeqNum uint64 + smallestSeqNum uint64 +} + +func (v *tableCacheValue) load(loadInfo loadInfo, c *tableCacheShard, dbOpts *tableCacheOpts) { // Try opening the file first. var f objstorage.Readable - f, v.err = dbOpts.objProvider.OpenForReadingMustExist(fileTypeTable, meta.FileNum) - if v.err == nil { - cacheOpts := private.SSTableCacheOpts(dbOpts.cacheID, meta.FileNum).(sstable.ReaderOption) - v.reader, v.err = sstable.NewReader(f, dbOpts.opts, cacheOpts, dbOpts.filterMetrics) + var err error + f, err = dbOpts.objProvider.OpenForReading( + context.TODO(), fileTypeTable, loadInfo.backingFileNum, objstorage.OpenOptions{MustExist: true}, + ) + if err == nil { + cacheOpts := private.SSTableCacheOpts(dbOpts.cacheID, loadInfo.backingFileNum).(sstable.ReaderOption) + v.reader, err = sstable.NewReader(f, dbOpts.opts, cacheOpts, dbOpts.filterMetrics) } - if v.err != nil { - v.err = errors.Wrapf(v.err, "pebble: backing file %s error", errors.Safe(meta.FileNum)) + if err != nil { + v.err = errors.Wrapf( + err, "pebble: backing file %s error", errors.Safe(loadInfo.backingFileNum.FileNum())) } - if v.err == nil { - if meta.SmallestSeqNum == meta.LargestSeqNum { - v.reader.Properties.GlobalSeqNum = meta.LargestSeqNum - } + if v.err == nil && loadInfo.smallestSeqNum == loadInfo.largestSeqNum { + v.reader.Properties.GlobalSeqNum = loadInfo.largestSeqNum } if v.err != nil { c.mu.Lock() defer c.mu.Unlock() // Lookup the node in the cache again as it might have already been // removed. - key := tableCacheKey{dbOpts.cacheID, meta.FileNum} + key := tableCacheKey{dbOpts.cacheID, loadInfo.backingFileNum} n := c.mu.nodes[key] if n != nil && n.value == v { c.releaseNode(n) @@ -990,8 +1147,8 @@ func (p tableCacheNodeType) String() string { } type tableCacheNode struct { - meta *fileMetadata - value *tableCacheValue + fileNum base.DiskFileNum + value *tableCacheValue links struct { next *tableCacheNode @@ -1000,7 +1157,7 @@ type tableCacheNode struct { ptype tableCacheNodeType // referenced is atomically set to indicate that this entry has been accessed // since the last time one of the clock hands swept it. - referenced int32 + referenced atomic.Bool // Storing the cache id associated with the DB instance here // avoids the need to thread the dbOpts struct through many functions. diff --git a/vendor/github.com/cockroachdb/pebble/table_stats.go b/vendor/github.com/cockroachdb/pebble/table_stats.go index 7c025e9..f149f44 100644 --- a/vendor/github.com/cockroachdb/pebble/table_stats.go +++ b/vendor/github.com/cockroachdb/pebble/table_stats.go @@ -53,7 +53,7 @@ func (d *DB) maybeCollectTableStatsLocked() { func (d *DB) updateTableStatsLocked(newFiles []manifest.NewFileEntry) { var needStats bool for _, nf := range newFiles { - if !nf.Meta.StatsValidLocked() { + if !nf.Meta.StatsValid() { needStats = true break } @@ -175,7 +175,7 @@ func (d *DB) loadNewFileStats( // collectTableStats updates f.Stats for active files, and we // ensure only one goroutine runs it at a time through // d.mu.tableStats.loading. - if nf.Meta.StatsValidLocked() { + if nf.Meta.StatsValid() { continue } @@ -186,7 +186,10 @@ func (d *DB) loadNewFileStats( continue } - stats, newHints, err := d.loadTableStats(rs.current, nf.Level, nf.Meta) + stats, newHints, err := d.loadTableStats( + rs.current, nf.Level, + nf.Meta, + ) if err != nil { d.opts.EventListener.BackgroundError(err) continue @@ -219,7 +222,7 @@ func (d *DB) scanReadStateTableStats( // and we ensure only one goroutine runs it at a time through // d.mu.tableStats.loading. This makes it safe to read validity // through f.Stats.ValidLocked despite not holding d.mu. - if f.StatsValidLocked() { + if f.StatsValid() { continue } @@ -233,7 +236,9 @@ func (d *DB) scanReadStateTableStats( return fill, hints, moreRemain } - stats, newHints, err := d.loadTableStats(rs.current, l, f) + stats, newHints, err := d.loadTableStats( + rs.current, l, f, + ) if err != nil { // Set `moreRemain` so we'll try again. moreRemain = true @@ -255,26 +260,30 @@ func (d *DB) loadTableStats( ) (manifest.TableStats, []deleteCompactionHint, error) { var stats manifest.TableStats var compactionHints []deleteCompactionHint - err := d.tableCache.withReader(meta, func(r *sstable.Reader) (err error) { - stats.NumEntries = r.Properties.NumEntries - stats.NumDeletions = r.Properties.NumDeletions - if r.Properties.NumPointDeletions() > 0 { - if err = d.loadTablePointKeyStats(r, v, level, meta, &stats); err != nil { - return + err := d.tableCache.withCommonReader( + meta, func(r sstable.CommonReader) (err error) { + props := r.CommonProperties() + stats.NumEntries = props.NumEntries + stats.NumDeletions = props.NumDeletions + if props.NumPointDeletions() > 0 { + if err = d.loadTablePointKeyStats(props, v, level, meta, &stats); err != nil { + return + } } - } - if r.Properties.NumRangeDeletions > 0 || r.Properties.NumRangeKeyDels > 0 { - if compactionHints, err = d.loadTableRangeDelStats(r, v, level, meta, &stats); err != nil { - return + if props.NumRangeDeletions > 0 || props.NumRangeKeyDels > 0 { + if compactionHints, err = d.loadTableRangeDelStats( + r, v, level, meta, &stats, + ); err != nil { + return + } } - } - // TODO(travers): Once we have real-world data, consider collecting - // additional stats that may provide improved heuristics for compaction - // picking. - stats.NumRangeKeySets = r.Properties.NumRangeKeySets - stats.ValueBlocksSize = r.Properties.ValueBlocksSize - return - }) + // TODO(travers): Once we have real-world data, consider collecting + // additional stats that may provide improved heuristics for compaction + // picking. + stats.NumRangeKeySets = props.NumRangeKeySets + stats.ValueBlocksSize = props.ValueBlocksSize + return + }) if err != nil { return stats, nil, err } @@ -284,7 +293,11 @@ func (d *DB) loadTableStats( // loadTablePointKeyStats calculates the point key statistics for the given // table. The provided manifest.TableStats are updated. func (d *DB) loadTablePointKeyStats( - r *sstable.Reader, v *version, level int, meta *fileMetadata, stats *manifest.TableStats, + props *sstable.CommonProperties, + v *version, + level int, + meta *fileMetadata, + stats *manifest.TableStats, ) error { // TODO(jackson): If the file has a wide keyspace, the average // value size beneath the entire file might not be representative @@ -292,19 +305,19 @@ func (d *DB) loadTablePointKeyStats( // We could write the ranges of 'clusters' of point tombstones to // a sstable property and call averageValueSizeBeneath for each of // these narrower ranges to improve the estimate. - avgKeySize, avgValSize, err := d.averageEntrySizeBeneath(v, level, meta) + avgValLogicalSize, compressionRatio, err := d.estimateSizesBeneath(v, level, meta, props) if err != nil { return err } stats.PointDeletionsBytesEstimate = - pointDeletionsBytesEstimate(&r.Properties, avgKeySize, avgValSize) + pointDeletionsBytesEstimate(meta.Size, props, avgValLogicalSize, compressionRatio) return nil } // loadTableRangeDelStats calculates the range deletion and range key deletion // statistics for the given table. func (d *DB) loadTableRangeDelStats( - r *sstable.Reader, v *version, level int, meta *fileMetadata, stats *manifest.TableStats, + r sstable.CommonReader, v *version, level int, meta *fileMetadata, stats *manifest.TableStats, ) ([]deleteCompactionHint, error) { iter, err := newCombinedDeletionKeyspanIter(d.opts.Comparer, r, meta) if err != nil { @@ -320,25 +333,50 @@ func (d *DB) loadTableRangeDelStats( for s := iter.First(); s != nil; s = iter.Next() { start, end := s.Start, s.End // We only need to consider deletion size estimates for tables that contain - // point keys. - var hasPoints bool + // RANGEDELs. + var maxRangeDeleteSeqNum uint64 for _, k := range s.Keys { - if k.Kind() == base.InternalKeyKindRangeDelete { - hasPoints = true + if k.Kind() == base.InternalKeyKindRangeDelete && maxRangeDeleteSeqNum < k.SeqNum() { + maxRangeDeleteSeqNum = k.SeqNum() break } } // If the file is in the last level of the LSM, there is no data beneath // it. The fact that there is still a range tombstone in a bottommost file - // suggests that an open snapshot kept the tombstone around. Estimate disk - // usage within the file itself. + // indicates two possibilites: + // 1. an open snapshot kept the tombstone around, and the data the + // tombstone deletes is contained within the file itself. + // 2. the file was ingested. + // In the first case, we'd like to estimate disk usage within the file + // itself since compacting the file will drop that covered data. In the + // second case, we expect that compacting the file will NOT drop any + // data and rewriting the file is a waste of write bandwidth. We can + // distinguish these cases by looking at the file metadata's sequence + // numbers. A file's range deletions can only delete data within the + // file at lower sequence numbers. All keys in an ingested sstable adopt + // the same sequence number, preventing tombstones from deleting keys + // within the same file. We check here if the largest RANGEDEL sequence + // number is greater than the file's smallest sequence number. If it is, + // the RANGEDEL could conceivably (although inconclusively) delete data + // within the same file. + // + // Note that this heuristic is imperfect. If a table containing a range + // deletion is ingested into L5 and subsequently compacted into L6 but + // an open snapshot prevents elision of covered keys in L6, the + // resulting RangeDeletionsBytesEstimate will incorrectly include all + // covered keys. + // + // TODO(jackson): We could prevent the above error in the heuristic by + // computing the file's RangeDeletionsBytesEstimate during the + // compaction itself. It's unclear how common this is. + // // NOTE: If the span `s` wholly contains a table containing range keys, // the returned size estimate will be slightly inflated by the range key // block. However, in practice, range keys are expected to be rare, and // the size of the range key block relative to the overall size of the // table is expected to be small. - if hasPoints && level == numLevels-1 { + if level == numLevels-1 && meta.SmallestSeqNum < maxRangeDeleteSeqNum { size, err := r.EstimateDiskUsage(start, end) if err != nil { return nil, err @@ -383,24 +421,49 @@ func (d *DB) loadTableRangeDelStats( return compactionHints, err } -func (d *DB) averageEntrySizeBeneath( - v *version, level int, meta *fileMetadata, -) (avgKeySize, avgValueSize uint64, err error) { +func (d *DB) estimateSizesBeneath( + v *version, level int, meta *fileMetadata, fileProps *sstable.CommonProperties, +) (avgValueLogicalSize, compressionRatio float64, err error) { // Find all files in lower levels that overlap with meta, // summing their value sizes and entry counts. + file := meta var fileSum, keySum, valSum, entryCount uint64 + // Include the file itself. This is important because in some instances, the + // computed compression ratio is applied to the tombstones contained within + // `meta` itself. If there are no files beneath `meta` in the LSM, we would + // calculate a compression ratio of 0 which is not accurate for the file's + // own tombstones. + fileSum += file.Size + entryCount += fileProps.NumEntries + keySum += fileProps.RawKeySize + valSum += fileProps.RawValueSize + + addPhysicalTableStats := func(r *sstable.Reader) (err error) { + fileSum += file.Size + entryCount += r.Properties.NumEntries + keySum += r.Properties.RawKeySize + valSum += r.Properties.RawValueSize + return nil + } + addVirtualTableStats := func(v sstable.VirtualReader) (err error) { + fileSum += file.Size + entryCount += file.Stats.NumEntries + keySum += v.Properties.RawKeySize + valSum += v.Properties.RawValueSize + return nil + } + for l := level + 1; l < numLevels; l++ { overlaps := v.Overlaps(l, d.cmp, meta.Smallest.UserKey, meta.Largest.UserKey, meta.Largest.IsExclusiveSentinel()) iter := overlaps.Iter() - for file := iter.First(); file != nil; file = iter.Next() { - err := d.tableCache.withReader(file, func(r *sstable.Reader) (err error) { - fileSum += file.Size - entryCount += r.Properties.NumEntries - keySum += r.Properties.RawKeySize - valSum += r.Properties.RawValueSize - return nil - }) + for file = iter.First(); file != nil; file = iter.Next() { + var err error + if file.Virtual { + err = d.tableCache.withVirtualReader(file.VirtualMeta(), addVirtualTableStats) + } else { + err = d.tableCache.withReader(file.PhysicalMeta(), addPhysicalTableStats) + } if err != nil { return 0, 0, err } @@ -409,24 +472,25 @@ func (d *DB) averageEntrySizeBeneath( if entryCount == 0 { return 0, 0, nil } - // RawKeySize and RawValueSize are uncompressed totals. Scale them - // according to the data size to account for compression, index blocks and - // metadata overhead. Eg: + // RawKeySize and RawValueSize are uncompressed totals. We'll need to scale + // the value sum according to the data size to account for compression, + // index blocks and metadata overhead. Eg: // - // Compression rate × Average uncompressed key size + // Compression rate × Average uncompressed value size // // ↓ // - // FileSize RawKeySize - // ----------------------- × ---------- + // FileSize RawValueSize + // ----------------------- × ------------ // RawKeySize+RawValueSize NumEntries // - // We refactor the calculation to avoid error from rounding/truncation. - totalSizePerEntry := fileSum / entryCount - uncompressedSum := keySum + valSum - avgKeySize = keySum * totalSizePerEntry / uncompressedSum - avgValueSize = valSum * totalSizePerEntry / uncompressedSum - return avgKeySize, avgValueSize, err + // We return the average logical value size plus the compression ratio, + // leaving the scaling to the caller. This allows the caller to perform + // additional compression ratio scaling if necessary. + uncompressedSum := float64(keySum + valSum) + compressionRatio = float64(fileSum) / uncompressedSum + avgValueLogicalSize = (float64(valSum) / float64(entryCount)) + return avgValueLogicalSize, compressionRatio, nil } func (d *DB) estimateReclaimedSizeBeneath( @@ -502,10 +566,21 @@ func (d *DB) estimateReclaimedSizeBeneath( continue } var size uint64 - err := d.tableCache.withReader(file, func(r *sstable.Reader) (err error) { - size, err = r.EstimateDiskUsage(start, end) - return err - }) + var err error + if file.Virtual { + err = d.tableCache.withVirtualReader( + file.VirtualMeta(), func(r sstable.VirtualReader) (err error) { + size, err = r.EstimateDiskUsage(start, end) + return err + }) + } else { + err = d.tableCache.withReader( + file.PhysicalMeta(), func(r *sstable.Reader) (err error) { + size, err = r.EstimateDiskUsage(start, end) + return err + }) + } + if err != nil { return 0, hintSeqNum, err } @@ -516,7 +591,7 @@ func (d *DB) estimateReclaimedSizeBeneath( return estimate, hintSeqNum, nil } -func maybeSetStatsFromProperties(meta *fileMetadata, props *sstable.Properties) bool { +func maybeSetStatsFromProperties(meta physicalMeta, props *sstable.Properties) bool { // If a table contains range deletions or range key deletions, we defer the // stats collection. There are two main reasons for this: // @@ -532,12 +607,12 @@ func maybeSetStatsFromProperties(meta *fileMetadata, props *sstable.Properties) return false } - // If a table is more than 10% point deletions, don't calculate the - // PointDeletionsBytesEstimate statistic using our limited knowledge. The - // table stats collector can populate the stats and calculate an average - // of value size of all the tables beneath the table in the LSM, which - // will be more accurate. - if props.NumDeletions > props.NumEntries/10 { + // If a table is more than 10% point deletions without user-provided size + // estimates, don't calculate the PointDeletionsBytesEstimate statistic + // using our limited knowledge. The table stats collector can populate the + // stats and calculate an average of value size of all the tables beneath + // the table in the LSM, which will be more accurate. + if unsizedDels := (props.NumDeletions - props.NumSizedDeletions); unsizedDels > props.NumEntries/10 { return false } @@ -547,8 +622,9 @@ func maybeSetStatsFromProperties(meta *fileMetadata, props *sstable.Properties) // doesn't require any additional IO and since the number of point // deletions in the file is low, the error introduced by this crude // estimate is expected to be small. - avgKeySize, avgValSize := estimateEntrySizes(meta.Size, props) - pointEstimate = pointDeletionsBytesEstimate(props, avgKeySize, avgValSize) + commonProps := &props.CommonProperties + avgValSize, compressionRatio := estimatePhysicalSizes(meta.Size, commonProps) + pointEstimate = pointDeletionsBytesEstimate(meta.Size, commonProps, avgValSize, compressionRatio) } meta.Stats.NumEntries = props.NumEntries @@ -561,47 +637,112 @@ func maybeSetStatsFromProperties(meta *fileMetadata, props *sstable.Properties) return true } -func pointDeletionsBytesEstimate(props *sstable.Properties, avgKeySize, avgValSize uint64) uint64 { +func pointDeletionsBytesEstimate( + fileSize uint64, props *sstable.CommonProperties, avgValLogicalSize, compressionRatio float64, +) (estimate uint64) { if props.NumEntries == 0 { return 0 } - // Estimate the potential space to reclaim using the table's own - // properties. There may or may not be keys covered by any individual - // point tombstone. If not, compacting the point tombstone into L6 will at - // least allow us to drop the point deletion key and will reclaim the key - // bytes. If there are covered key(s), we also get to drop key and value - // bytes for each covered key. + numPointDels := props.NumPointDeletions() + if numPointDels == 0 { + return 0 + } + // Estimate the potential space to reclaim using the table's own properties. + // There may or may not be keys covered by any individual point tombstone. + // If not, compacting the point tombstone into L6 will at least allow us to + // drop the point deletion key and will reclaim the tombstone's key bytes. + // If there are covered key(s), we also get to drop key and value bytes for + // each covered key. // - // We estimate assuming that each point tombstone on average covers 1 key. + // Some point tombstones (DELSIZEDs) carry a user-provided estimate of the + // uncompressed size of entries that will be elided by fully compacting the + // tombstone. For these tombstones, there's no guesswork—we use the + // RawPointTombstoneValueSizeHint property which is the sum of all these + // tombstones' encoded values. + // + // For un-sized point tombstones (DELs), we estimate assuming that each + // point tombstone on average covers 1 key and using average value sizes. // This is almost certainly an overestimate, but that's probably okay // because point tombstones can slow range iterations even when they don't - // cover a key. It may be beneficial in the future to more accurately - // estimate which tombstones cover keys and which do not. - numPointDels := props.NumPointDeletions() - return numPointDels*avgKeySize + numPointDels*(avgKeySize+avgValSize) + // cover a key. + // + // TODO(jackson): This logic doesn't directly incorporate fixed per-key + // overhead (8-byte trailer, plus at least 1 byte encoding the length of the + // key and 1 byte encoding the length of the value). This overhead is + // indirectly incorporated through the compression ratios, but that results + // in the overhead being smeared per key-byte and value-byte, rather than + // per-entry. This per-key fixed overhead can be nontrivial, especially for + // dense swaths of point tombstones. Give some thought as to whether we + // should directly include fixed per-key overhead in the calculations. + + // Below, we calculate the tombstone contributions and the shadowed keys' + // contributions separately. + var tombstonesLogicalSize float64 + var shadowedLogicalSize float64 + + // 1. Calculate the contribution of the tombstone keys themselves. + if props.RawPointTombstoneKeySize > 0 { + tombstonesLogicalSize += float64(props.RawPointTombstoneKeySize) + } else { + // This sstable predates the existence of the RawPointTombstoneKeySize + // property. We can use the average key size within the file itself and + // the count of point deletions to estimate the size. + tombstonesLogicalSize += float64(numPointDels * props.RawKeySize / props.NumEntries) + } + + // 2. Calculate the contribution of the keys shadowed by tombstones. + // + // 2a. First account for keys shadowed by DELSIZED tombstones. THE DELSIZED + // tombstones encode the size of both the key and value of the shadowed KV + // entries. These sizes are aggregated into a sstable property. + shadowedLogicalSize += float64(props.RawPointTombstoneValueSize) + + // 2b. Calculate the contribution of the KV entries shadowed by ordinary DEL + // keys. + numUnsizedDels := numPointDels - props.NumSizedDeletions + { + // The shadowed keys have the same exact user keys as the tombstones + // themselves, so we can use the `tombstonesLogicalSize` we computed + // earlier as an estimate. There's a complication that + // `tombstonesLogicalSize` may include DELSIZED keys we already + // accounted for. + shadowedLogicalSize += float64(tombstonesLogicalSize) / float64(numPointDels) * float64(numUnsizedDels) + + // Calculate the contribution of the deleted values. The caller has + // already computed an average logical size (possibly computed across + // many sstables). + shadowedLogicalSize += float64(numUnsizedDels) * avgValLogicalSize + } + + // Scale both tombstone and shadowed totals by logical:physical ratios to + // account for compression, metadata overhead, etc. + // + // Physical FileSize + // ----------- = ----------------------- + // Logical RawKeySize+RawValueSize + // + return uint64((tombstonesLogicalSize + shadowedLogicalSize) * compressionRatio) } -func estimateEntrySizes( - fileSize uint64, props *sstable.Properties, -) (avgKeySize, avgValSize uint64) { - // RawKeySize and RawValueSize are uncompressed totals. Scale them - // according to the data size to account for compression, index blocks and - // metadata overhead. Eg: +func estimatePhysicalSizes( + fileSize uint64, props *sstable.CommonProperties, +) (avgValLogicalSize, compressionRatio float64) { + // RawKeySize and RawValueSize are uncompressed totals. Scale according to + // the data size to account for compression, index blocks and metadata + // overhead. Eg: // - // Compression rate × Average uncompressed key size + // Compression rate × Average uncompressed value size // // ↓ // - // FileSize RawKeySize + // FileSize RawValSize // ----------------------- × ---------- // RawKeySize+RawValueSize NumEntries // - // We refactor the calculation to avoid error from rounding/truncation. - fileSizePerEntry := fileSize / props.NumEntries uncompressedSum := props.RawKeySize + props.RawValueSize - avgKeySize = props.RawKeySize * fileSizePerEntry / uncompressedSum - avgValSize = props.RawValueSize * fileSizePerEntry / uncompressedSum - return avgKeySize, avgValSize + compressionRatio = float64(fileSize) / float64(uncompressedSum) + avgValLogicalSize = (float64(props.RawValueSize) / float64(props.NumEntries)) + return avgValLogicalSize, compressionRatio } // newCombinedDeletionKeyspanIter returns a keyspan.FragmentIterator that @@ -657,7 +798,7 @@ func estimateEntrySizes( // corresponding to the largest and smallest sequence numbers encountered across // the range deletes and range keys deletes that comprised the merged spans. func newCombinedDeletionKeyspanIter( - comparer *base.Comparer, r *sstable.Reader, m *fileMetadata, + comparer *base.Comparer, cr sstable.CommonReader, m *fileMetadata, ) (keyspan.FragmentIterator, error) { // The range del iter and range key iter are each wrapped in their own // defragmenting iter. For each iter, abutting spans can always be merged. @@ -719,7 +860,7 @@ func newCombinedDeletionKeyspanIter( }) mIter.Init(comparer.Compare, transform, new(keyspan.MergingBuffers)) - iter, err := r.NewRawRangeDelIter() + iter, err := cr.NewRawRangeDelIter() if err != nil { return nil, err } @@ -730,12 +871,13 @@ func newCombinedDeletionKeyspanIter( // Truncate tombstones to the containing file's bounds if necessary. // See docs/range_deletions.md for why this is necessary. iter = keyspan.Truncate( - comparer.Compare, iter, m.Smallest.UserKey, m.Largest.UserKey, nil, nil, + comparer.Compare, iter, m.Smallest.UserKey, m.Largest.UserKey, + nil, nil, false, /* panicOnUpperTruncate */ ) mIter.AddLevel(iter) } - iter, err = r.NewRawRangeKeyIter() + iter, err = cr.NewRawRangeKeyIter() if err != nil { return nil, err } @@ -752,7 +894,7 @@ func newCombinedDeletionKeyspanIter( out.Keys = append(out.Keys, k) } return len(out.Keys) > 0 - }) + }, comparer.Compare) dIter := &keyspan.DefragmentingIter{} dIter.Init(comparer, iter, equal, reducer, new(keyspan.DefragmentingBuffers)) iter = dIter @@ -785,7 +927,7 @@ func (a rangeKeySetsAnnotator) Accumulate( ) (v interface{}, cacheOK bool) { vptr := dst.(*uint64) *vptr = *vptr + f.Stats.NumRangeKeySets - return vptr, f.StatsValidLocked() + return vptr, f.StatsValid() } func (a rangeKeySetsAnnotator) Merge(src interface{}, dst interface{}) interface{} { @@ -832,7 +974,7 @@ func (a tombstonesAnnotator) Accumulate( ) (v interface{}, cacheOK bool) { vptr := dst.(*uint64) *vptr = *vptr + f.Stats.NumDeletions - return vptr, f.StatsValidLocked() + return vptr, f.StatsValid() } func (a tombstonesAnnotator) Merge(src interface{}, dst interface{}) interface{} { @@ -879,7 +1021,7 @@ func (a valueBlocksSizeAnnotator) Accumulate( ) (v interface{}, cacheOK bool) { vptr := dst.(*uint64) *vptr = *vptr + f.Stats.ValueBlocksSize - return vptr, f.StatsValidLocked() + return vptr, f.StatsValid() } func (a valueBlocksSizeAnnotator) Merge(src interface{}, dst interface{}) interface{} { diff --git a/vendor/github.com/cockroachdb/pebble/version_set.go b/vendor/github.com/cockroachdb/pebble/version_set.go index 216f211..5f4c677 100644 --- a/vendor/github.com/cockroachdb/pebble/version_set.go +++ b/vendor/github.com/cockroachdb/pebble/version_set.go @@ -8,7 +8,6 @@ import ( "bytes" "fmt" "io" - "math" "sync" "sync/atomic" @@ -30,6 +29,9 @@ const manifestMarkerName = `manifest` type bulkVersionEdit = manifest.BulkVersionEdit type deletedFileEntry = manifest.DeletedFileEntry type fileMetadata = manifest.FileMetadata +type physicalMeta = manifest.PhysicalFileMeta +type virtualMeta = manifest.VirtualFileMeta +type fileBacking = manifest.FileBacking type newFileEntry = manifest.NewFileEntry type version = manifest.Version type versionEdit = manifest.VersionEdit @@ -41,36 +43,28 @@ type versionList = manifest.VersionList // like it sounds: a delta from the previous version. Version edits are logged // to the MANIFEST file, which is replayed at startup. type versionSet struct { - // WARNING: The following struct `atomic` contains fields are accessed atomically. - // - // Go allocations are guaranteed to be 64-bit aligned which we take advantage - // of by placing the 64-bit fields which we access atomically at the beginning - // of the versionSet struct. - // For more information, see https://golang.org/pkg/sync/atomic/#pkg-note-BUG. - atomic struct { - logSeqNum uint64 // next seqNum to use for WAL writes + // Next seqNum to use for WAL writes. + logSeqNum atomic.Uint64 - // The upper bound on sequence numbers that have been assigned so far. - // A suffix of these sequence numbers may not have been written to a - // WAL. Both logSeqNum and visibleSeqNum are atomically updated by the - // commitPipeline. - visibleSeqNum uint64 // visible seqNum (<= logSeqNum) + // The upper bound on sequence numbers that have been assigned so far. A + // suffix of these sequence numbers may not have been written to a WAL. Both + // logSeqNum and visibleSeqNum are atomically updated by the commitPipeline. + // visibleSeqNum is <= logSeqNum. + visibleSeqNum atomic.Uint64 - // Number of bytes present in sstables being written by in-progress - // compactions. This value will be zero if there are no in-progress - // compactions. Updated and read atomically. - atomicInProgressBytes int64 - } + // Number of bytes present in sstables being written by in-progress + // compactions. This value will be zero if there are no in-progress + // compactions. Updated and read atomically. + atomicInProgressBytes atomic.Int64 // Immutable fields. dirname string // Set to DB.mu. - mu *sync.Mutex - opts *Options - fs vfs.FS - cmp Compare - cmpName string - diskAvailBytes func() uint64 + mu *sync.Mutex + opts *Options + fs vfs.FS + cmp Compare + cmpName string // Dynamic base level allows the dynamic base level computation to be // disabled. Used by tests which want to create specific LSM structures. dynamicBaseLevel bool @@ -83,14 +77,30 @@ type versionSet struct { // A pointer to versionSet.addObsoleteLocked. Avoids allocating a new closure // on the creation of every version. - obsoleteFn func(obsolete []*manifest.FileMetadata) - obsoleteTables []*manifest.FileMetadata + obsoleteFn func(obsolete []*fileBacking) + obsoleteTables []fileInfo obsoleteManifests []fileInfo obsoleteOptions []fileInfo // Zombie tables which have been removed from the current version but are // still referenced by an inuse iterator. - zombieTables map[FileNum]uint64 // filenum -> size + zombieTables map[base.DiskFileNum]uint64 // filenum -> size + + // backingState is protected by the versionSet.logLock. It's populated + // during Open in versionSet.load, but it's not used concurrently during + // load. + backingState struct { + // fileBackingMap is a map for the FileBacking which is supporting virtual + // sstables in the latest version. Once the file backing is backing no + // virtual sstables in the latest version, it is removed from this map and + // the corresponding state is added to the zombieTables map. Note that we + // don't keep track of file backing which supports a virtual sstable + // which is not in the latest version. + fileBackingMap map[base.DiskFileNum]*fileBacking + // fileBackingSize is the sum of the sizes of the fileBackings in the + // fileBackingMap. + fileBackingSize uint64 + } // minUnflushedLogNum is the smallest WAL log file number corresponding to // mutations that have not been flushed to an sstable. @@ -104,9 +114,10 @@ type versionSet struct { manifestFileNum FileNum manifestMarker *atomicfs.Marker - manifestFile vfs.File - manifest *record.Writer - setCurrent func(FileNum) error + manifestFile vfs.File + manifest *record.Writer + setCurrent func(FileNum) error + getFormatMajorVersion func() FormatMajorVersion writing bool writerCond sync.Cond @@ -119,6 +130,7 @@ func (vs *versionSet) init( opts *Options, marker *atomicfs.Marker, setCurrent func(FileNum) error, + getFMV func() FormatMajorVersion, mu *sync.Mutex, ) { vs.dirname = dirname @@ -131,13 +143,13 @@ func (vs *versionSet) init( vs.dynamicBaseLevel = true vs.versions.Init(mu) vs.obsoleteFn = vs.addObsoleteLocked - vs.zombieTables = make(map[FileNum]uint64) + vs.zombieTables = make(map[base.DiskFileNum]uint64) + vs.backingState.fileBackingMap = make(map[base.DiskFileNum]*fileBacking) + vs.backingState.fileBackingSize = 0 vs.nextFileNum = 1 vs.manifestMarker = marker vs.setCurrent = setCurrent - if vs.diskAvailBytes == nil { - vs.diskAvailBytes = func() uint64 { return math.MaxUint64 } - } + vs.getFormatMajorVersion = getFMV } // create creates a version set for a fresh DB. @@ -147,15 +159,15 @@ func (vs *versionSet) create( opts *Options, marker *atomicfs.Marker, setCurrent func(FileNum) error, + getFormatMajorVersion func() FormatMajorVersion, mu *sync.Mutex, ) error { - vs.init(dirname, opts, marker, setCurrent, mu) + vs.init(dirname, opts, marker, setCurrent, getFormatMajorVersion, mu) newVersion := &version{} vs.append(newVersion) var err error - vs.picker = newCompactionPicker(newVersion, vs.opts, nil, vs.metrics.levelSizes(), vs.diskAvailBytes) - + vs.picker = newCompactionPicker(newVersion, vs.opts, nil) // Note that a "snapshot" version edit is written to the manifest when it is // created. vs.manifestFileNum = vs.getNextFileNum() @@ -179,7 +191,7 @@ func (vs *versionSet) create( vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{ JobID: jobID, - Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, vs.manifestFileNum), + Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, vs.manifestFileNum.DiskFileNum()), FileNum: vs.manifestFileNum, Err: err, }) @@ -196,12 +208,13 @@ func (vs *versionSet) load( manifestFileNum FileNum, marker *atomicfs.Marker, setCurrent func(FileNum) error, + getFormatMajorVersion func() FormatMajorVersion, mu *sync.Mutex, ) error { - vs.init(dirname, opts, marker, setCurrent, mu) + vs.init(dirname, opts, marker, setCurrent, getFormatMajorVersion, mu) vs.manifestFileNum = manifestFileNum - manifestPath := base.MakeFilepath(opts.FS, dirname, fileTypeManifest, vs.manifestFileNum) + manifestPath := base.MakeFilepath(opts.FS, dirname, fileTypeManifest, vs.manifestFileNum.DiskFileNum()) manifestFilename := opts.FS.PathBase(manifestPath) // Read the versionEdits in the manifest file. @@ -257,7 +270,14 @@ func (vs *versionSet) load( // (assuming no WALs contain higher sequence numbers than the // manifest's LastSeqNum). Increment LastSeqNum by 1 to get the // next sequence number that will be assigned. - vs.atomic.logSeqNum = ve.LastSeqNum + 1 + // + // If LastSeqNum is less than SeqNumStart, increase it to at least + // SeqNumStart to leave ample room for reserved sequence numbers. + if ve.LastSeqNum+1 < base.SeqNumStart { + vs.logSeqNum.Store(base.SeqNumStart) + } else { + vs.logSeqNum.Store(ve.LastSeqNum + 1) + } } } // We have already set vs.nextFileNum = 2 at the beginning of the @@ -277,7 +297,21 @@ func (vs *versionSet) load( } vs.markFileNumUsed(vs.minUnflushedLogNum) - newVersion, _, err := bve.Apply(nil, vs.cmp, opts.Comparer.FormatKey, opts.FlushSplitBytes, opts.Experimental.ReadCompactionRate) + // Populate the fileBackingMap and the FileBacking for virtual sstables since + // we have finished version edit accumulation. + for _, s := range bve.AddedFileBacking { + vs.addFileBacking(s) + } + + for _, fileNum := range bve.RemovedFileBacking { + vs.removeFileBacking(fileNum) + } + + newVersion, err := bve.Apply( + nil, vs.cmp, opts.Comparer.FormatKey, opts.FlushSplitBytes, + opts.Experimental.ReadCompactionRate, nil, /* zombies */ + getFormatMajorVersion().orderingInvariants(), + ) if err != nil { return err } @@ -291,7 +325,7 @@ func (vs *versionSet) load( l.Size = int64(files.SizeSum()) } - vs.picker = newCompactionPicker(newVersion, vs.opts, nil, vs.metrics.levelSizes(), vs.diskAvailBytes) + vs.picker = newCompactionPicker(newVersion, vs.opts, nil) return nil } @@ -334,6 +368,26 @@ func (vs *versionSet) logUnlock() { vs.writerCond.Signal() } +// Only call if the DiskFileNum doesn't exist in the fileBackingMap. +func (vs *versionSet) addFileBacking(backing *manifest.FileBacking) { + _, ok := vs.backingState.fileBackingMap[backing.DiskFileNum] + if ok { + panic("pebble: trying to add an existing file backing") + } + vs.backingState.fileBackingMap[backing.DiskFileNum] = backing + vs.backingState.fileBackingSize += backing.Size +} + +// Only call if the the DiskFileNum exists in the fileBackingMap. +func (vs *versionSet) removeFileBacking(dfn base.DiskFileNum) { + backing, ok := vs.backingState.fileBackingMap[dfn] + if !ok { + panic("pebble: trying to remove an unknown file backing") + } + delete(vs.backingState.fileBackingMap, dfn) + vs.backingState.fileBackingSize -= backing.Size +} + // logAndApply logs the version edit to the manifest, applies the version edit // to the current version, and installs the new version. // @@ -382,7 +436,7 @@ func (vs *versionSet) logAndApply( // in an unflushed memtable. logSeqNum is the _next_ sequence number that // will be assigned, so subtract that by 1 to get the upper bound on the // last assigned sequence number. - logSeqNum := atomic.LoadUint64(&vs.atomic.logSeqNum) + logSeqNum := vs.logSeqNum.Load() ve.LastSeqNum = logSeqNum - 1 if logSeqNum == 0 { // logSeqNum is initialized to 1 in Open() if there are no previous WAL @@ -391,6 +445,8 @@ func (vs *versionSet) logAndApply( } currentVersion := vs.currentVersion() + fmv := vs.getFormatMajorVersion() + orderingInvariants := fmv.orderingInvariants() var newVersion *version // Generate a new manifest if we don't currently have one, or forceRotation @@ -455,18 +511,21 @@ func (vs *versionSet) logAndApply( minUnflushedLogNum := vs.minUnflushedLogNum nextFileNum := vs.nextFileNum - var zombies map[FileNum]uint64 + var zombies map[base.DiskFileNum]uint64 if err := func() error { vs.mu.Unlock() defer vs.mu.Lock() - var bve bulkVersionEdit - if err := bve.Accumulate(ve); err != nil { - return err - } - var err error - newVersion, zombies, err = bve.Apply(currentVersion, vs.cmp, vs.opts.Comparer.FormatKey, vs.opts.FlushSplitBytes, vs.opts.Experimental.ReadCompactionRate) + if vs.getFormatMajorVersion() < FormatVirtualSSTables && len(ve.CreatedBackingTables) > 0 { + return errors.AssertionFailedf("MANIFEST cannot contain virtual sstable records due to format major version") + } + newVersion, zombies, err = manifest.AccumulateIncompleteAndApplySingleVE( + ve, currentVersion, vs.cmp, vs.opts.Comparer.FormatKey, + vs.opts.FlushSplitBytes, vs.opts.Experimental.ReadCompactionRate, + vs.backingState.fileBackingMap, vs.addFileBacking, vs.removeFileBacking, + orderingInvariants, + ) if err != nil { return errors.Wrap(err, "MANIFEST apply failed") } @@ -475,7 +534,7 @@ func (vs *versionSet) logAndApply( if err := vs.createManifest(vs.dirname, newManifestFileNum, minUnflushedLogNum, nextFileNum); err != nil { vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{ JobID: jobID, - Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, newManifestFileNum), + Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, newManifestFileNum.DiskFileNum()), FileNum: newManifestFileNum, Err: err, }) @@ -487,7 +546,8 @@ func (vs *versionSet) logAndApply( if err != nil { return errors.Wrap(err, "MANIFEST next record write failed") } - // NB: Any error from this point on is considered fatal as we don't now if + + // NB: Any error from this point on is considered fatal as we don't know if // the MANIFEST write occurred or not. Trying to determine that is // fraught. Instead we rely on the standard recovery mechanism run when a // database is open. In particular, that mechanism generates a new MANIFEST @@ -508,7 +568,7 @@ func (vs *versionSet) logAndApply( } vs.opts.EventListener.ManifestCreated(ManifestCreateInfo{ JobID: jobID, - Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, newManifestFileNum), + Path: base.MakeFilepath(vs.fs, vs.dirname, fileTypeManifest, newManifestFileNum.DiskFileNum()), FileNum: newManifestFileNum, }) } @@ -549,7 +609,7 @@ func (vs *versionSet) logAndApply( if newManifestFileNum != 0 { if vs.manifestFileNum != 0 { vs.obsoleteManifests = append(vs.obsoleteManifests, fileInfo{ - fileNum: vs.manifestFileNum, + fileNum: vs.manifestFileNum.DiskFileNum(), fileSize: prevManifestFileSize, }) } @@ -561,30 +621,46 @@ func (vs *versionSet) logAndApply( } for i := range vs.metrics.Levels { l := &vs.metrics.Levels[i] + l.NumFiles = int64(newVersion.Levels[i].Len()) + l.NumVirtualFiles = newVersion.Levels[i].NumVirtual + l.VirtualSize = newVersion.Levels[i].VirtualSize + l.Size = int64(newVersion.Levels[i].Size()) + l.Sublevels = 0 if l.NumFiles > 0 { l.Sublevels = 1 } if invariants.Enabled { - if count := int64(newVersion.Levels[i].Len()); l.NumFiles != count { - vs.opts.Logger.Fatalf("versionSet metrics L%d NumFiles = %d, actual count = %d", i, l.NumFiles, count) - } levelFiles := newVersion.Levels[i].Slice() if size := int64(levelFiles.SizeSum()); l.Size != size { vs.opts.Logger.Fatalf("versionSet metrics L%d Size = %d, actual size = %d", i, l.Size, size) } + if nVirtual := levelFiles.NumVirtual(); nVirtual != l.NumVirtualFiles { + vs.opts.Logger.Fatalf( + "versionSet metrics L%d NumVirtual = %d, actual NumVirtual = %d", + i, l.NumVirtualFiles, nVirtual, + ) + } + if vSize := levelFiles.VirtualSizeSum(); vSize != l.VirtualSize { + vs.opts.Logger.Fatalf( + "versionSet metrics L%d Virtual size = %d, actual size = %d", + i, l.VirtualSize, vSize, + ) + } } } vs.metrics.Levels[0].Sublevels = int32(len(newVersion.L0SublevelFiles)) - vs.picker = newCompactionPicker(newVersion, vs.opts, inProgress, vs.metrics.levelSizes(), vs.diskAvailBytes) + vs.picker = newCompactionPicker(newVersion, vs.opts, inProgress) if !vs.dynamicBaseLevel { vs.picker.forceBaseLevel1() } return nil } -func (vs *versionSet) incrementCompactions(kind compactionKind, extraLevels []*compactionLevel) { +func (vs *versionSet) incrementCompactions( + kind compactionKind, extraLevels []*compactionLevel, pickerMetrics compactionPickerMetrics, +) { switch kind { case compactionKindDefault: vs.metrics.Compact.Count++ @@ -619,7 +695,7 @@ func (vs *versionSet) incrementCompactions(kind compactionKind, extraLevels []*c } func (vs *versionSet) incrementCompactionBytes(numBytes int64) { - atomic.AddInt64(&vs.atomic.atomicInProgressBytes, numBytes) + vs.atomicInProgressBytes.Add(numBytes) } // createManifest creates a manifest file that contains a snapshot of vs. @@ -627,7 +703,7 @@ func (vs *versionSet) createManifest( dirname string, fileNum, minUnflushedLogNum, nextFileNum FileNum, ) (err error) { var ( - filename = base.MakeFilepath(vs.fs, dirname, fileTypeManifest, fileNum) + filename = base.MakeFilepath(vs.fs, dirname, fileTypeManifest, fileNum.DiskFileNum()) manifestFile vfs.File manifest *record.Writer ) @@ -651,6 +727,7 @@ func (vs *versionSet) createManifest( snapshot := versionEdit{ ComparerName: vs.cmpName, } + dedup := make(map[base.DiskFileNum]struct{}) for level, levelMetadata := range vs.currentVersion().Levels { iter := levelMetadata.Iter() for meta := iter.First(); meta != nil; meta = iter.Next() { @@ -658,6 +735,13 @@ func (vs *versionSet) createManifest( Level: level, Meta: meta, }) + if _, ok := dedup[meta.FileBacking.DiskFileNum]; meta.Virtual && !ok { + dedup[meta.FileBacking.DiskFileNum] = struct{}{} + snapshot.CreatedBackingTables = append( + snapshot.CreatedBackingTables, + meta.FileBacking, + ) + } } } @@ -722,13 +806,13 @@ func (vs *versionSet) currentVersion() *version { return vs.versions.Back() } -func (vs *versionSet) addLiveFileNums(m map[FileNum]struct{}) { +func (vs *versionSet) addLiveFileNums(m map[base.DiskFileNum]struct{}) { current := vs.currentVersion() for v := vs.versions.Front(); true; v = v.Next() { for _, lm := range v.Levels { iter := lm.Iter() for f := iter.First(); f != nil; f = iter.Next() { - m[f.FileNum] = struct{}{} + m[f.FileBacking.DiskFileNum] = struct{}{} } } if v == current { @@ -737,27 +821,49 @@ func (vs *versionSet) addLiveFileNums(m map[FileNum]struct{}) { } } +// addObsoleteLocked will add the fileInfo associated with obsolete backing +// sstables to the obsolete tables list. +// +// The file backings in the obsolete list must not appear more than once. +// // DB.mu must be held when addObsoleteLocked is called. -func (vs *versionSet) addObsoleteLocked(obsolete []*manifest.FileMetadata) { +func (vs *versionSet) addObsoleteLocked(obsolete []*fileBacking) { if len(obsolete) == 0 { return } - for _, fileMeta := range obsolete { + obsoleteFileInfo := make([]fileInfo, len(obsolete)) + for i, bs := range obsolete { + obsoleteFileInfo[i].fileNum = bs.DiskFileNum + obsoleteFileInfo[i].fileSize = bs.Size + } + + if invariants.Enabled { + dedup := make(map[base.DiskFileNum]struct{}) + for _, fi := range obsoleteFileInfo { + dedup[fi.fileNum] = struct{}{} + } + if len(dedup) != len(obsoleteFileInfo) { + panic("pebble: duplicate FileBacking present in obsolete list") + } + } + + for _, fi := range obsoleteFileInfo { // Note that the obsolete tables are no longer zombie by the definition of // zombie, but we leave them in the zombie tables map until they are // deleted from disk. - if _, ok := vs.zombieTables[fileMeta.FileNum]; !ok { - vs.opts.Logger.Fatalf("MANIFEST obsolete table %s not marked as zombie", fileMeta.FileNum) + if _, ok := vs.zombieTables[fi.fileNum]; !ok { + vs.opts.Logger.Fatalf("MANIFEST obsolete table %s not marked as zombie", fi.fileNum) } } - vs.obsoleteTables = append(vs.obsoleteTables, obsolete...) + + vs.obsoleteTables = append(vs.obsoleteTables, obsoleteFileInfo...) vs.updateObsoleteTableMetricsLocked() } // addObsolete will acquire DB.mu, so DB.mu must not be held when this is // called. -func (vs *versionSet) addObsolete(obsolete []*manifest.FileMetadata) { +func (vs *versionSet) addObsolete(obsolete []*fileBacking) { vs.mu.Lock() defer vs.mu.Unlock() vs.addObsoleteLocked(obsolete) @@ -766,8 +872,8 @@ func (vs *versionSet) addObsolete(obsolete []*manifest.FileMetadata) { func (vs *versionSet) updateObsoleteTableMetricsLocked() { vs.metrics.Table.ObsoleteCount = int64(len(vs.obsoleteTables)) vs.metrics.Table.ObsoleteSize = 0 - for _, fileMeta := range vs.obsoleteTables { - vs.metrics.Table.ObsoleteSize += fileMeta.Size + for _, fi := range vs.obsoleteTables { + vs.metrics.Table.ObsoleteSize += fi.fileSize } } @@ -779,7 +885,7 @@ func setCurrentFunc( // the CURRENT file to signal which MANIFEST is current. Ignore // the filename read during LocateMarker. return func(manifestFileNum FileNum) error { - if err := setCurrentFile(dirname, fs, manifestFileNum); err != nil { + if err := setCurrentFile(dirname, fs, manifestFileNum.DiskFileNum()); err != nil { return err } if err := dir.Sync(); err != nil { @@ -797,13 +903,13 @@ func setCurrentFunc( func setCurrentFuncMarker(marker *atomicfs.Marker, fs vfs.FS, dirname string) func(FileNum) error { return func(manifestFileNum FileNum) error { - return marker.Move(base.MakeFilename(fileTypeManifest, manifestFileNum)) + return marker.Move(base.MakeFilename(fileTypeManifest, manifestFileNum.DiskFileNum())) } } func findCurrentManifest( vers FormatMajorVersion, fs vfs.FS, dirname string, -) (marker *atomicfs.Marker, manifestNum FileNum, exists bool, err error) { +) (marker *atomicfs.Marker, manifestNum base.DiskFileNum, exists bool, err error) { // NB: We always locate the manifest marker, even if we might not // actually use it (because we're opening the database at an earlier // format major version that uses the CURRENT file). Locating a @@ -811,7 +917,7 @@ func findCurrentManifest( var filename string marker, filename, err = atomicfs.LocateMarker(fs, dirname, manifestMarkerName) if err != nil { - return nil, 0, false, err + return nil, base.FileNum(0).DiskFileNum(), false, err } if vers < formatVersionedManifestMarker { @@ -821,9 +927,9 @@ func findCurrentManifest( manifestNum, err = readCurrentFile(fs, dirname) if oserror.IsNotExist(err) { - return marker, 0, false, nil + return marker, base.FileNum(0).DiskFileNum(), false, nil } else if err != nil { - return marker, 0, false, err + return marker, base.FileNum(0).DiskFileNum(), false, err } return marker, manifestNum, true, nil } @@ -834,48 +940,48 @@ func findCurrentManifest( if filename == "" { // The marker hasn't been set yet. This database doesn't exist. - return marker, 0, false, nil + return marker, base.FileNum(0).DiskFileNum(), false, nil } var ok bool _, manifestNum, ok = base.ParseFilename(fs, filename) if !ok { - return marker, 0, false, base.CorruptionErrorf("pebble: MANIFEST name %q is malformed", errors.Safe(filename)) + return marker, base.FileNum(0).DiskFileNum(), false, base.CorruptionErrorf("pebble: MANIFEST name %q is malformed", errors.Safe(filename)) } return marker, manifestNum, true, nil } -func readCurrentFile(fs vfs.FS, dirname string) (FileNum, error) { +func readCurrentFile(fs vfs.FS, dirname string) (base.DiskFileNum, error) { // Read the CURRENT file to find the current manifest file. - current, err := fs.Open(base.MakeFilepath(fs, dirname, fileTypeCurrent, 0)) + current, err := fs.Open(base.MakeFilepath(fs, dirname, fileTypeCurrent, base.FileNum(0).DiskFileNum())) if err != nil { - return 0, errors.Wrapf(err, "pebble: could not open CURRENT file for DB %q", dirname) + return base.FileNum(0).DiskFileNum(), errors.Wrapf(err, "pebble: could not open CURRENT file for DB %q", dirname) } defer current.Close() stat, err := current.Stat() if err != nil { - return 0, err + return base.FileNum(0).DiskFileNum(), err } n := stat.Size() if n == 0 { - return 0, errors.Errorf("pebble: CURRENT file for DB %q is empty", dirname) + return base.FileNum(0).DiskFileNum(), errors.Errorf("pebble: CURRENT file for DB %q is empty", dirname) } if n > 4096 { - return 0, errors.Errorf("pebble: CURRENT file for DB %q is too large", dirname) + return base.FileNum(0).DiskFileNum(), errors.Errorf("pebble: CURRENT file for DB %q is too large", dirname) } b := make([]byte, n) _, err = current.ReadAt(b, 0) if err != nil { - return 0, err + return base.FileNum(0).DiskFileNum(), err } if b[n-1] != '\n' { - return 0, base.CorruptionErrorf("pebble: CURRENT file for DB %q is malformed", dirname) + return base.FileNum(0).DiskFileNum(), base.CorruptionErrorf("pebble: CURRENT file for DB %q is malformed", dirname) } b = bytes.TrimSpace(b) _, manifestFileNum, ok := base.ParseFilename(fs, string(b)) if !ok { - return 0, base.CorruptionErrorf("pebble: MANIFEST name %q is malformed", errors.Safe(b)) + return base.FileNum(0).DiskFileNum(), base.CorruptionErrorf("pebble: MANIFEST name %q is malformed", errors.Safe(b)) } return manifestFileNum, nil } diff --git a/vendor/github.com/cockroachdb/pebble/vfs/default_linux_noarm.go b/vendor/github.com/cockroachdb/pebble/vfs/default_linux.go similarity index 98% rename from vendor/github.com/cockroachdb/pebble/vfs/default_linux_noarm.go rename to vendor/github.com/cockroachdb/pebble/vfs/default_linux.go index 2e6c5e6..ec29074 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/default_linux_noarm.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/default_linux.go @@ -2,8 +2,8 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -//go:build linux && !arm -// +build linux,!arm +//go:build linux +// +build linux package vfs diff --git a/vendor/github.com/cockroachdb/pebble/vfs/default_linux_arm.go b/vendor/github.com/cockroachdb/pebble/vfs/default_linux_arm.go deleted file mode 100644 index 338b3c0..0000000 --- a/vendor/github.com/cockroachdb/pebble/vfs/default_linux_arm.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2023 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. - -//go:build linux && arm -// +build linux,arm - -package vfs - -import ( - "os" - "syscall" - - "github.com/cockroachdb/errors" - "golang.org/x/sys/unix" -) - -func wrapOSFileImpl(f *os.File) File { - lf := &linuxOnArmFile{File: f, fd: f.Fd()} - if lf.fd != InvalidFd { - lf.useSyncRange = isSyncRangeSupported(lf.fd) - } - return lf -} - -func (defaultFS) OpenDir(name string) (File, error) { - f, err := os.OpenFile(name, syscall.O_CLOEXEC, 0) - if err != nil { - return nil, errors.WithStack(err) - } - return &linuxOnArmDir{f}, nil -} - -// Assert that linuxOnArmFile and linuxOnArmDir implement vfs.File. -var ( - _ File = (*linuxOnArmDir)(nil) - _ File = (*linuxOnArmFile)(nil) -) - -type linuxOnArmDir struct { - *os.File -} - -func (d *linuxOnArmDir) Prefetch(offset int64, length int64) error { return nil } -func (d *linuxOnArmDir) Preallocate(offset, length int64) error { return nil } -func (d *linuxOnArmDir) SyncData() error { return d.Sync() } -func (d *linuxOnArmDir) SyncTo(offset int64) (fullSync bool, err error) { return false, nil } - -type linuxOnArmFile struct { - *os.File - fd uintptr - useSyncRange bool -} - -func (f *linuxOnArmFile) Prefetch(offset int64, length int64) error { - _, _, err := unix.Syscall(unix.SYS_READAHEAD, uintptr(f.fd), uintptr(offset), uintptr(length)) - return err -} - -func (f *linuxOnArmFile) Preallocate(offset, length int64) error { return nil } - -func (f *linuxOnArmFile) SyncData() error { - // TODO(radu): does arm support unix.Fdatasync? - return f.Sync() -} - -func (f *linuxOnArmFile) SyncTo(int64) (fullSync bool, err error) { - // syscall.SyncFileRange is not defined form arm (see https://github.com/cockroachdb/pebble/issues/171). - if err = f.Sync(); err != nil { - return false, err - } - return true, nil -} diff --git a/vendor/github.com/cockroachdb/pebble/vfs/disk_full.go b/vendor/github.com/cockroachdb/pebble/vfs/disk_full.go index c6542a0..16f6a5d 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/disk_full.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/disk_full.go @@ -39,68 +39,66 @@ func OnDiskFull(fs FS, fn func()) FS { } type enospcFS struct { - inner FS - atomic struct { - // generation is a monotonically increasing number that encodes the - // current state of ENOSPC error handling. Incoming writes are - // organized into generations to provide strong guarantees on when the - // disk full callback is invoked. The callback is invoked once per - // write generation. - // - // Special significance is given to the parity of this generation - // field to optimize incoming writes in the normal state, which only - // need to perform a single atomic load. If generation is odd, an - // ENOSPC error is being actively handled. The generations associated - // with writes are always even. - // - // The lifecycle of a write is: - // - // 1. Atomically load the current generation. - // a. If it's even, this is the write's generation number. - // b. If it's odd, an ENOSPC was recently encountered and the - // corresponding invocation of the disk full callback has not - // yet completed. The write must wait until the callback has - // completed and generation is updated to an even number, which - // becomes the write's generation number. - // 2. Perform the write. If it encounters no error or an error other - // than ENOSPC, the write returns and proceeds no further in this - // lifecycle. - // 3. Handle ENOSPC. If the write encounters ENOSPC, the callback must - // be invoked for the write's generation. The write's goroutine - // acquires the FS's mutex. - // a. If the FS's current generation is still equal to the write's - // generation, the write is the first write of its generation to - // encounter ENOSPC. It increments the FS's current generation - // to an odd number, signifying that an ENOSPC is being handled - // and invokes the callback. - // b. If the FS's current generation has changed, some other write - // from the same generation encountered an ENOSPC first. This - // write waits on the condition variable until the FS's current - // generation is updated indicating that the generation's - // callback invocation has completed. - // 3. Retry the write once. The callback for the write's generation - // has completed, either by this write's goroutine or another's. - // The write may proceed with the expectation that the callback - // remedied the full disk by freeing up disk space and an ENOSPC - // should not be encountered again for at least a few minutes. If - // we do encounter another ENOSPC on the retry, the callback was - // unable to remedy the full disk and another retry won't be - // useful. Any error, including ENOSPC, during the retry is - // returned without further handling. None of the retries invoke - // the callback. - // - // This scheme has a few nice properties: - // * Once the disk-full callback completes, it won't be invoked - // again unless a write that started strictly later encounters an - // ENOSPC. This is convenient if the callback strives to 'fix' the - // full disk, for example, by removing a ballast file. A new - // invocation of the callback guarantees a new problem. - // * Incoming writes block if there's an unhandled ENOSPC. Some - // writes, like WAL or MANIFEST fsyncs, are fatal if they encounter - // an ENOSPC. - generation uint32 - } - mu struct { + inner FS + // generation is a monotonically increasing number that encodes the + // current state of ENOSPC error handling. Incoming writes are + // organized into generations to provide strong guarantees on when the + // disk full callback is invoked. The callback is invoked once per + // write generation. + // + // Special significance is given to the parity of this generation + // field to optimize incoming writes in the normal state, which only + // need to perform a single atomic load. If generation is odd, an + // ENOSPC error is being actively handled. The generations associated + // with writes are always even. + // + // The lifecycle of a write is: + // + // 1. Atomically load the current generation. + // a. If it's even, this is the write's generation number. + // b. If it's odd, an ENOSPC was recently encountered and the + // corresponding invocation of the disk full callback has not + // yet completed. The write must wait until the callback has + // completed and generation is updated to an even number, which + // becomes the write's generation number. + // 2. Perform the write. If it encounters no error or an error other + // than ENOSPC, the write returns and proceeds no further in this + // lifecycle. + // 3. Handle ENOSPC. If the write encounters ENOSPC, the callback must + // be invoked for the write's generation. The write's goroutine + // acquires the FS's mutex. + // a. If the FS's current generation is still equal to the write's + // generation, the write is the first write of its generation to + // encounter ENOSPC. It increments the FS's current generation + // to an odd number, signifying that an ENOSPC is being handled + // and invokes the callback. + // b. If the FS's current generation has changed, some other write + // from the same generation encountered an ENOSPC first. This + // write waits on the condition variable until the FS's current + // generation is updated indicating that the generation's + // callback invocation has completed. + // 3. Retry the write once. The callback for the write's generation + // has completed, either by this write's goroutine or another's. + // The write may proceed with the expectation that the callback + // remedied the full disk by freeing up disk space and an ENOSPC + // should not be encountered again for at least a few minutes. If + // we do encounter another ENOSPC on the retry, the callback was + // unable to remedy the full disk and another retry won't be + // useful. Any error, including ENOSPC, during the retry is + // returned without further handling. None of the retries invoke + // the callback. + // + // This scheme has a few nice properties: + // * Once the disk-full callback completes, it won't be invoked + // again unless a write that started strictly later encounters an + // ENOSPC. This is convenient if the callback strives to 'fix' the + // full disk, for example, by removing a ballast file. A new + // invocation of the callback guarantees a new problem. + // * Incoming writes block if there's an unhandled ENOSPC. Some + // writes, like WAL or MANIFEST fsyncs, are fatal if they encounter + // an ENOSPC. + generation atomic.Uint32 + mu struct { sync.Mutex sync.Cond onDiskFull func() @@ -119,7 +117,7 @@ func (fs *enospcFS) Unwrap() FS { // waitUntilReady blocks until the callback returns. The returned generation // is always even. func (fs *enospcFS) waitUntilReady() uint32 { - gen := atomic.LoadUint32(&fs.atomic.generation) + gen := fs.generation.Load() if gen%2 == 0 { // An even generation indicates that we're not currently handling an // ENOSPC. Allow the write to proceed. @@ -132,10 +130,10 @@ func (fs *enospcFS) waitUntilReady() uint32 { defer fs.mu.Unlock() // Load the generation again with fs.mu locked. - gen = atomic.LoadUint32(&fs.atomic.generation) + gen = fs.generation.Load() for gen%2 == 1 { fs.mu.Wait() - gen = atomic.LoadUint32(&fs.atomic.generation) + gen = fs.generation.Load() } return gen } @@ -144,7 +142,7 @@ func (fs *enospcFS) handleENOSPC(gen uint32) { fs.mu.Lock() defer fs.mu.Unlock() - currentGeneration := atomic.LoadUint32(&fs.atomic.generation) + currentGeneration := fs.generation.Load() // If the current generation is still `gen`, this is the first goroutine // to hit an ENOSPC within this write generation, so this goroutine is @@ -153,7 +151,7 @@ func (fs *enospcFS) handleENOSPC(gen uint32) { // Increment the generation to an odd number, indicating that the FS // is out-of-disk space and incoming writes should pause and wait for // the next generation before continuing. - atomic.StoreUint32(&fs.atomic.generation, gen+1) + fs.generation.Store(gen + 1) func() { // Drop the mutex while we invoke the callback, re-acquiring @@ -165,7 +163,7 @@ func (fs *enospcFS) handleENOSPC(gen uint32) { // Update the current generation again to an even number, indicating // that the callback has completed for the write generation `gen`. - atomic.StoreUint32(&fs.atomic.generation, gen+2) + fs.generation.Store(gen + 2) fs.mu.Broadcast() return } @@ -185,7 +183,7 @@ func (fs *enospcFS) handleENOSPC(gen uint32) { // subsequent generations' callbacks. for currentGeneration == gen+1 { fs.mu.Wait() - currentGeneration = atomic.LoadUint32(&fs.atomic.generation) + currentGeneration = fs.generation.Load() } } @@ -230,6 +228,17 @@ func (fs *enospcFS) Open(name string, opts ...OpenOption) (File, error) { return f, err } +func (fs *enospcFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) { + f, err := fs.inner.OpenReadWrite(name, opts...) + if f != nil { + f = &enospcFile{ + fs: fs, + inner: f, + } + } + return f, err +} + func (fs *enospcFS) OpenDir(name string) (File, error) { f, err := fs.inner.OpenDir(name) if f != nil { @@ -377,6 +386,20 @@ func (f *enospcFile) Write(p []byte) (n int, err error) { return n, err } +func (f *enospcFile) WriteAt(p []byte, ofs int64) (n int, err error) { + gen := f.fs.waitUntilReady() + + n, err = f.inner.WriteAt(p, ofs) + + if err != nil && isENOSPC(err) { + f.fs.handleENOSPC(gen) + var n2 int + n2, err = f.inner.WriteAt(p[n:], ofs+int64(n)) + n += n2 + } + return n, err +} + func (f *enospcFile) Prefetch(offset, length int64) error { return f.inner.Prefetch(offset, length) } diff --git a/vendor/github.com/cockroachdb/pebble/vfs/disk_health.go b/vendor/github.com/cockroachdb/pebble/vfs/disk_health.go index 1c0b9c5..b7d328e 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/disk_health.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/disk_health.go @@ -13,7 +13,6 @@ import ( "sync/atomic" "time" - "github.com/cockroachdb/pebble/internal/invariants" "github.com/cockroachdb/redact" ) @@ -135,7 +134,7 @@ type diskHealthCheckingFile struct { // // NB: this packing scheme is not persisted, and is therefore safe to adjust // across process boundaries. - lastWritePacked uint64 + lastWritePacked atomic.Uint64 createTime time.Time } @@ -174,7 +173,7 @@ func (d *diskHealthCheckingFile) startTicker() { return case <-ticker.C: - packed := atomic.LoadUint64(&d.lastWritePacked) + packed := d.lastWritePacked.Load() if packed == 0 { continue } @@ -219,6 +218,14 @@ func (d *diskHealthCheckingFile) Write(p []byte) (n int, err error) { return n, err } +// Write implements the io.WriterAt interface. +func (d *diskHealthCheckingFile) WriteAt(p []byte, ofs int64) (n int, err error) { + d.timeDiskOp(OpTypeWrite, int64(len(p)), func() { + n, err = d.file.WriteAt(p, ofs) + }) + return n, err +} + // Close implements the io.Closer interface. func (d *diskHealthCheckingFile) Close() error { d.stopTicker() @@ -279,20 +286,12 @@ func (d *diskHealthCheckingFile) timeDiskOp(opType OpType, writeSizeInBytes int6 delta := time.Since(d.createTime) packed := pack(delta, writeSizeInBytes, opType) - if invariants.Enabled { - if !atomic.CompareAndSwapUint64(&d.lastWritePacked, 0, packed) { - panic("concurrent write operations detected on file") - } - } else { - atomic.StoreUint64(&d.lastWritePacked, packed) + if d.lastWritePacked.Swap(packed) != 0 { + panic("concurrent write operations detected on file") } defer func() { - if invariants.Enabled { - if !atomic.CompareAndSwapUint64(&d.lastWritePacked, packed, 0) { - panic("concurrent write operations detected on file") - } - } else { - atomic.StoreUint64(&d.lastWritePacked, 0) + if d.lastWritePacked.Swap(0) != packed { + panic("concurrent write operations detected on file") } }() op() @@ -375,9 +374,17 @@ func (i DiskSlowInfo) String() string { // SafeFormat implements redact.SafeFormatter. func (i DiskSlowInfo) SafeFormat(w redact.SafePrinter, _ rune) { - w.Printf("disk slowness detected: %s on file %s (%d bytes) has been ongoing for %0.1fs", - redact.Safe(i.OpType.String()), redact.Safe(filepath.Base(i.Path)), - redact.Safe(i.WriteSize), redact.Safe(i.Duration.Seconds())) + switch i.OpType { + // Operations for which i.WriteSize is meaningful. + case OpTypeWrite, OpTypeSyncTo, OpTypePreallocate: + w.Printf("disk slowness detected: %s on file %s (%d bytes) has been ongoing for %0.1fs", + redact.Safe(i.OpType.String()), redact.Safe(filepath.Base(i.Path)), + redact.Safe(i.WriteSize), redact.Safe(i.Duration.Seconds())) + default: + w.Printf("disk slowness detected: %s on file %s has been ongoing for %0.1fs", + redact.Safe(i.OpType.String()), redact.Safe(filepath.Base(i.Path)), + redact.Safe(i.Duration.Seconds())) + } } // diskHealthCheckingFS adds disk-health checking facilities to a VFS. @@ -443,7 +450,7 @@ type diskHealthCheckingFS struct { type slot struct { name string opType OpType - startNanos int64 + startNanos atomic.Int64 } // diskHealthCheckingFS implements FS. @@ -510,12 +517,12 @@ func (d *diskHealthCheckingFS) timeFilesystemOp(name string, opType OpType, op f startNanos := time.Now().UnixNano() for i := 0; i < len(d.mu.inflight); i++ { - if atomic.LoadInt64(&d.mu.inflight[i].startNanos) == 0 { + if d.mu.inflight[i].startNanos.Load() == 0 { // This slot is not in use. Claim it. s = d.mu.inflight[i] s.name = name s.opType = opType - atomic.StoreInt64(&s.startNanos, startNanos) + s.startNanos.Store(startNanos) break } } @@ -526,10 +533,10 @@ func (d *diskHealthCheckingFS) timeFilesystemOp(name string, opType OpType, op f // incur an allocation. if s == nil { s = &slot{ - name: name, - opType: opType, - startNanos: startNanos, + name: name, + opType: opType, } + s.startNanos.Store(startNanos) d.mu.inflight = append(d.mu.inflight, s) } }() @@ -537,7 +544,7 @@ func (d *diskHealthCheckingFS) timeFilesystemOp(name string, opType OpType, op f op() // Signal completion by zeroing the start time. - atomic.StoreInt64(&s.startNanos, 0) + s.startNanos.Store(0) } // startTickerLocked starts a new goroutine with a ticker to monitor disk @@ -548,7 +555,12 @@ func (d *diskHealthCheckingFS) startTickerLocked() { go func() { ticker := time.NewTicker(d.tickInterval) defer ticker.Stop() - var exceededSlots []slot + type exceededSlot struct { + name string + opType OpType + startNanos int64 + } + var exceededSlots []exceededSlot for { select { @@ -559,14 +571,15 @@ func (d *diskHealthCheckingFS) startTickerLocked() { d.mu.Lock() now := time.Now() for i := range d.mu.inflight { - nanos := atomic.LoadInt64(&d.mu.inflight[i].startNanos) + nanos := d.mu.inflight[i].startNanos.Load() if nanos != 0 && time.Unix(0, nanos).Add(d.diskSlowThreshold).Before(now) { // diskSlowThreshold was exceeded. Copy this inflightOp into // exceededSlots and call d.onSlowDisk after dropping the mutex. - var inflightOp slot - inflightOp.name = d.mu.inflight[i].name - inflightOp.opType = d.mu.inflight[i].opType - inflightOp.startNanos = nanos + inflightOp := exceededSlot{ + name: d.mu.inflight[i].name, + opType: d.mu.inflight[i].opType, + startNanos: nanos, + } exceededSlots = append(exceededSlots, inflightOp) } } @@ -680,6 +693,11 @@ func (d *diskHealthCheckingFS) Open(name string, opts ...OpenOption) (File, erro return d.fs.Open(name, opts...) } +// OpenReadWrite implements the FS interface. +func (d *diskHealthCheckingFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) { + return d.fs.OpenReadWrite(name, opts...) +} + // OpenDir implements the FS interface. func (d *diskHealthCheckingFS) OpenDir(name string) (File, error) { f, err := d.fs.OpenDir(name) diff --git a/vendor/github.com/cockroachdb/pebble/vfs/disk_usage_openbsd.go b/vendor/github.com/cockroachdb/pebble/vfs/disk_usage_openbsd.go new file mode 100644 index 0000000..19e9c5c --- /dev/null +++ b/vendor/github.com/cockroachdb/pebble/vfs/disk_usage_openbsd.go @@ -0,0 +1,26 @@ +// Copyright 2023 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. + +//go:build openbsd +// +build openbsd + +package vfs + +import "golang.org/x/sys/unix" + +func (defaultFS) GetDiskUsage(path string) (DiskUsage, error) { + stat := unix.Statfs_t{} + if err := unix.Statfs(path, &stat); err != nil { + return DiskUsage{}, err + } + + freeBytes := uint64(stat.F_bsize) * uint64(stat.F_bfree) + availBytes := uint64(stat.F_bsize) * uint64(stat.F_bavail) + totalBytes := uint64(stat.F_bsize) * uint64(stat.F_blocks) + return DiskUsage{ + AvailBytes: availBytes, + TotalBytes: totalBytes, + UsedBytes: totalBytes - freeBytes, + }, nil +} diff --git a/vendor/github.com/cockroachdb/pebble/vfs/disk_usage_unix.go b/vendor/github.com/cockroachdb/pebble/vfs/disk_usage_unix.go index 9990241..95d9a7c 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/disk_usage_unix.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/disk_usage_unix.go @@ -2,8 +2,8 @@ // of this source code is governed by a BSD-style license that can be found in // the LICENSE file. -//go:build darwin || openbsd || dragonfly || freebsd -// +build darwin openbsd dragonfly freebsd +//go:build darwin || dragonfly || freebsd +// +build darwin dragonfly freebsd package vfs diff --git a/vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go b/vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go index ee0c3e2..820c081 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/logging_fs.go @@ -37,6 +37,24 @@ func (fs *loggingFS) Create(name string) (File, error) { return newLoggingFile(f, name, fs.logFn), nil } +func (fs *loggingFS) Open(name string, opts ...OpenOption) (File, error) { + fs.logFn("open: %s", name) + f, err := fs.FS.Open(name, opts...) + if err != nil { + return nil, err + } + return newLoggingFile(f, name, fs.logFn), nil +} + +func (fs *loggingFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) { + fs.logFn("open-read-write: %s", name) + f, err := fs.FS.OpenReadWrite(name, opts...) + if err != nil { + return nil, err + } + return newLoggingFile(f, name, fs.logFn), nil +} + func (fs *loggingFS) Link(oldname, newname string) error { fs.logFn("link: %s -> %s", oldname, newname) return fs.FS.Link(oldname, newname) @@ -122,3 +140,18 @@ func (f *loggingFile) SyncTo(length int64) (fullSync bool, err error) { f.logFn("sync-to(%d): %s", length, f.name) return f.File.SyncTo(length) } + +func (f *loggingFile) ReadAt(p []byte, offset int64) (int, error) { + f.logFn("read-at(%d, %d): %s", offset, len(p), f.name) + return f.File.ReadAt(p, offset) +} + +func (f *loggingFile) WriteAt(p []byte, offset int64) (int, error) { + f.logFn("write-at(%d, %d): %s", offset, len(p), f.name) + return f.File.WriteAt(p, offset) +} + +func (f *loggingFile) Prefetch(offset int64, length int64) error { + f.logFn("prefetch(%d, %d): %s", offset, length, f.name) + return f.File.Prefetch(offset, length) +} diff --git a/vendor/github.com/cockroachdb/pebble/vfs/mem_fs.go b/vendor/github.com/cockroachdb/pebble/vfs/mem_fs.go index 5b072da..ef6c87d 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/mem_fs.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/mem_fs.go @@ -14,6 +14,7 @@ import ( "strings" "sync" "sync/atomic" + "syscall" "time" "github.com/cockroachdb/errors" @@ -63,7 +64,8 @@ func NewStrictMem() *MemFS { // NewMemFile returns a memory-backed File implementation. The memory-backed // file takes ownership of data. func NewMemFile(data []byte) File { - n := &memNode{refs: 1} + n := &memNode{} + n.refs.Store(1) n.mu.data = data n.mu.modTime = time.Now() return &memFile{ @@ -77,6 +79,11 @@ type MemFS struct { mu sync.Mutex root *memNode + // lockFiles holds a map of open file locks. Presence in this map indicates + // a file lock is currently held. Keys are strings holding the path of the + // locked file. The stored value is untyped and unused; only presence of + // the key within the map is significant. + lockedFiles sync.Map strict bool ignoreSyncs bool // Windows has peculiar semantics with respect to hard links and deleting @@ -208,6 +215,7 @@ func (y *MemFS) Create(fullname string) (File, error) { ret = &memFile{ n: n, fs: y, + read: true, write: true, } } @@ -216,7 +224,7 @@ func (y *MemFS) Create(fullname string) (File, error) { if err != nil { return nil, err } - atomic.AddInt32(&ret.n.refs, 1) + ret.n.refs.Add(1) return ret, nil } @@ -262,7 +270,7 @@ func (y *MemFS) Link(oldname, newname string) error { }) } -func (y *MemFS) open(fullname string) (File, error) { +func (y *MemFS) open(fullname string, openForWrite bool) (File, error) { var ret *memFile err := y.walk(fullname, func(dir *memNode, frag string, final bool) error { if final { @@ -275,9 +283,10 @@ func (y *MemFS) open(fullname string) (File, error) { } if n := dir.children[frag]; n != nil { ret = &memFile{ - n: n, - fs: y, - read: true, + n: n, + fs: y, + read: true, + write: openForWrite, } } } @@ -293,18 +302,28 @@ func (y *MemFS) open(fullname string) (File, error) { Err: oserror.ErrNotExist, } } - atomic.AddInt32(&ret.n.refs, 1) + ret.n.refs.Add(1) return ret, nil } // Open implements FS.Open. func (y *MemFS) Open(fullname string, opts ...OpenOption) (File, error) { - return y.open(fullname) + return y.open(fullname, false /* openForWrite */) +} + +// OpenReadWrite implements FS.OpenReadWrite. +func (y *MemFS) OpenReadWrite(fullname string, opts ...OpenOption) (File, error) { + f, err := y.open(fullname, true /* openForWrite */) + pathErr, ok := err.(*os.PathError) + if ok && pathErr.Err == oserror.ErrNotExist { + return y.Create(fullname) + } + return f, err } // OpenDir implements FS.OpenDir. func (y *MemFS) OpenDir(fullname string) (File, error) { - return y.open(fullname) + return y.open(fullname, false /* openForWrite */) } // Remove implements FS.Remove. @@ -323,7 +342,7 @@ func (y *MemFS) Remove(fullname string) error { // Windows semantics. This ensures that we don't regress in the // ordering of operations and try to remove a file while it is // still open. - if n := atomic.LoadInt32(&child.refs); n > 0 { + if n := child.refs.Load(); n > 0 { return oserror.ErrInvalid } } @@ -444,9 +463,31 @@ func (y *MemFS) MkdirAll(dirname string, perm os.FileMode) error { // Lock implements FS.Lock. func (y *MemFS) Lock(fullname string) (io.Closer, error) { // FS.Lock excludes other processes, but other processes cannot see this - // process' memory. We translate Lock into Create so that have the normal - // detection of non-existent directory paths. - return y.Create(fullname) + // process' memory. However some uses (eg, Cockroach tests) may open and + // close the same MemFS-backed database multiple times. We want mutual + // exclusion in this case too. See cockroachdb/cockroach#110645. + _, loaded := y.lockedFiles.Swap(fullname, nil /* the value itself is insignificant */) + if loaded { + // This file lock has already been acquired. On unix, this results in + // either EACCES or EAGAIN so we mimic. + return nil, syscall.EAGAIN + } + // Otherwise, we successfully acquired the lock. Locks are visible in the + // parent directory listing, and they also must be created under an existent + // directory. Create the path so that we have the normal detection of + // non-existent directory paths, and make the lock visible when listing + // directory entries. + f, err := y.Create(fullname) + if err != nil { + // "Release" the lock since we failed. + y.lockedFiles.Delete(fullname) + return nil, err + } + return &memFileLock{ + y: y, + f: f, + fullname: fullname, + }, nil } // List implements FS.List. @@ -513,7 +554,7 @@ func (*MemFS) GetDiskUsage(string) (DiskUsage, error) { type memNode struct { name string isDir bool - refs int32 + refs atomic.Int32 // Mutable state. // - For a file: data, syncedDate, modTime: A file is only being mutated by a single goroutine, @@ -629,7 +670,7 @@ type memFile struct { var _ File = (*memFile)(nil) func (f *memFile) Close() error { - if n := atomic.AddInt32(&f.n.refs, -1); n < 0 { + if n := f.n.refs.Add(-1); n < 0 { panic(fmt.Sprintf("pebble: close of unopened file: %d", n)) } f.n = nil @@ -665,7 +706,11 @@ func (f *memFile) ReadAt(p []byte, off int64) (int, error) { if off >= int64(len(f.n.mu.data)) { return 0, io.EOF } - return copy(p, f.n.mu.data[off:]), nil + n := copy(p, f.n.mu.data[off:]) + if n < len(p) { + return n, io.EOF + } + return n, nil } func (f *memFile) Write(p []byte) (int, error) { @@ -698,6 +743,29 @@ func (f *memFile) Write(p []byte) (int, error) { return len(p), nil } +func (f *memFile) WriteAt(p []byte, ofs int64) (int, error) { + if !f.write { + return 0, errors.New("pebble/vfs: file was not created for writing") + } + if f.n.isDir { + return 0, errors.New("pebble/vfs: cannot write a directory") + } + f.n.mu.Lock() + defer f.n.mu.Unlock() + f.n.mu.modTime = time.Now() + + for len(f.n.mu.data) < int(ofs)+len(p) { + f.n.mu.data = append(f.n.mu.data, 0) + } + + n := copy(f.n.mu.data[int(ofs):int(ofs)+len(p)], p) + if n != len(p) { + panic("stuff") + } + + return len(p), nil +} + func (f *memFile) Prefetch(offset int64, length int64) error { return nil } func (f *memFile) Preallocate(offset, length int64) error { return nil } @@ -747,3 +815,18 @@ func (f *memFile) Fd() uintptr { func (f *memFile) Flush() error { return nil } + +type memFileLock struct { + y *MemFS + f File + fullname string +} + +func (l *memFileLock) Close() error { + if l.y == nil { + return nil + } + l.y.lockedFiles.Delete(l.fullname) + l.y = nil + return l.f.Close() +} diff --git a/vendor/github.com/cockroachdb/pebble/vfs/syncing_file.go b/vendor/github.com/cockroachdb/pebble/vfs/syncing_file.go index d1a4ae1..cf59862 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/syncing_file.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/syncing_file.go @@ -26,16 +26,14 @@ type syncingFile struct { noSyncOnClose bool bytesPerSync int64 preallocateSize int64 - atomic struct { - // The offset at which dirty data has been written. - offset int64 - // The offset at which data has been synced. Note that if SyncFileRange is - // being used, the periodic syncing of data during writing will only ever - // sync up to offset-1MB. This is done to avoid rewriting the tail of the - // file multiple times, but has the side effect of ensuring that Close will - // sync the file's metadata. - syncOffset int64 - } + // The offset at which dirty data has been written. + offset atomic.Int64 + // The offset at which data has been synced. Note that if SyncFileRange is + // being used, the periodic syncing of data during writing will only ever + // sync up to offset-1MB. This is done to avoid rewriting the tail of the + // file multiple times, but has the side effect of ensuring that Close will + // sync the file's metadata. + syncOffset atomic.Int64 preallocatedBlocks int64 } @@ -54,13 +52,13 @@ func NewSyncingFile(f File, opts SyncingFileOptions) File { } // Ensure a file that is opened and then closed will be synced, even if no // data has been written to it. - s.atomic.syncOffset = -1 + s.syncOffset.Store(-1) return s } // NB: syncingFile.Write is unsafe for concurrent use! func (f *syncingFile) Write(p []byte) (n int, err error) { - _ = f.preallocate(atomic.LoadInt64(&f.atomic.offset)) + _ = f.preallocate(f.offset.Load()) n, err = f.File.Write(p) if err != nil { @@ -68,7 +66,7 @@ func (f *syncingFile) Write(p []byte) (n int, err error) { } // The offset is updated atomically so that it can be accessed safely from // Sync. - atomic.AddInt64(&f.atomic.offset, int64(n)) + f.offset.Add(int64(n)) if err := f.maybeSync(); err != nil { return 0, err } @@ -93,11 +91,11 @@ func (f *syncingFile) preallocate(offset int64) error { func (f *syncingFile) ratchetSyncOffset(offset int64) { for { - syncOffset := atomic.LoadInt64(&f.atomic.syncOffset) + syncOffset := f.syncOffset.Load() if syncOffset >= offset { return } - if atomic.CompareAndSwapInt64(&f.atomic.syncOffset, syncOffset, offset) { + if f.syncOffset.CompareAndSwap(syncOffset, offset) { return } } @@ -109,7 +107,7 @@ func (f *syncingFile) Sync() error { // offset, we still need to call the underlying file's sync for persistence // guarantees which are not provided by SyncTo (or by sync_file_range on // Linux). - f.ratchetSyncOffset(atomic.LoadInt64(&f.atomic.offset)) + f.ratchetSyncOffset(f.offset.Load()) return f.SyncData() } @@ -127,7 +125,7 @@ func (f *syncingFile) maybeSync() error { // Xfs does neighbor page flushing outside of the specified ranges. We // need to make sure sync range is far from the write offset. const syncRangeBuffer = 1 << 20 // 1 MB - offset := atomic.LoadInt64(&f.atomic.offset) + offset := f.offset.Load() if offset <= syncRangeBuffer { return nil } @@ -135,7 +133,7 @@ func (f *syncingFile) maybeSync() error { const syncRangeAlignment = 4 << 10 // 4 KB syncToOffset := offset - syncRangeBuffer syncToOffset -= syncToOffset % syncRangeAlignment - syncOffset := atomic.LoadInt64(&f.atomic.syncOffset) + syncOffset := f.syncOffset.Load() if syncToOffset < 0 || (syncToOffset-syncOffset) < f.bytesPerSync { return nil } @@ -167,7 +165,7 @@ func (f *syncingFile) Close() error { // caller has not called Sync since the last write, syncOffset is guaranteed // to be less than atomic.offset. This ensures we fall into the below // conditional and perform a full sync to durably persist the file. - if off := atomic.LoadInt64(&f.atomic.offset); off > atomic.LoadInt64(&f.atomic.syncOffset) { + if off := f.offset.Load(); off > f.syncOffset.Load() { // There's still remaining dirty data. if f.noSyncOnClose { diff --git a/vendor/github.com/cockroachdb/pebble/vfs/vfs.go b/vendor/github.com/cockroachdb/pebble/vfs/vfs.go index 7eafbf6..3cc0b43 100644 --- a/vendor/github.com/cockroachdb/pebble/vfs/vfs.go +++ b/vendor/github.com/cockroachdb/pebble/vfs/vfs.go @@ -29,6 +29,8 @@ type File interface { // method *is* allowed to modify the slice passed in, whether temporarily // or permanently. Callers of Write() need to take this into account. io.Writer + // WriteAt() is only supported for files that were opened with FS.OpenReadWrite. + io.WriterAt // Preallocate optionally preallocates storage for `length` at `offset` // within the file. Implementations may choose to do nothing. @@ -99,6 +101,10 @@ type FS interface { // Open opens the named file for reading. openOptions provides Open(name string, opts ...OpenOption) (File, error) + // OpenReadWrite opens the named file for reading and writing. If the file + // does not exist, it is created. + OpenReadWrite(name string, opts ...OpenOption) (File, error) + // OpenDir opens the named directory for syncing. OpenDir(name string) (File, error) @@ -232,6 +238,18 @@ func (defaultFS) Open(name string, opts ...OpenOption) (File, error) { return file, nil } +func (defaultFS) OpenReadWrite(name string, opts ...OpenOption) (File, error) { + osFile, err := os.OpenFile(name, os.O_RDWR|syscall.O_CLOEXEC|os.O_CREATE, 0666) + if err != nil { + return nil, errors.WithStack(err) + } + file := wrapOSFile(osFile) + for _, opt := range opts { + opt.Apply(file) + } + return file, nil +} + func (defaultFS) Remove(name string) error { return errors.WithStack(os.Remove(name)) } diff --git a/vendor/github.com/cockroachdb/tokenbucket/LICENSE b/vendor/github.com/cockroachdb/tokenbucket/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/vendor/github.com/cockroachdb/tokenbucket/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/github.com/cockroachdb/tokenbucket/README.md b/vendor/github.com/cockroachdb/tokenbucket/README.md new file mode 100644 index 0000000..edf49ba --- /dev/null +++ b/vendor/github.com/cockroachdb/tokenbucket/README.md @@ -0,0 +1,2 @@ +# tokenbucket +Token bucket implementation in Go diff --git a/vendor/github.com/cockroachdb/tokenbucket/token_bucket.go b/vendor/github.com/cockroachdb/tokenbucket/token_bucket.go new file mode 100644 index 0000000..6a8159f --- /dev/null +++ b/vendor/github.com/cockroachdb/tokenbucket/token_bucket.go @@ -0,0 +1,202 @@ +// Copyright 2023 The Cockroach Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +// implied. See the License for the specific language governing +// permissions and limitations under the License. + +package tokenbucket + +import ( + "context" + "time" +) + +// Tokens are abstract units (usually units of work). +type Tokens float64 + +// TokensPerSecond is the rate of token replenishment. +type TokensPerSecond float64 + +// TokenBucket implements the basic accounting for a token bucket. +// +// A token bucket has a rate of replenishment and a burst limit. Tokens are +// replenished over time, up to the burst limit. +// +// The token bucket keeps track of the current amount and updates it as time +// passes. The bucket can go into debt (i.e. negative current amount). +type TokenBucket struct { + rate TokensPerSecond + burst Tokens + nowFn func() time.Time + + current Tokens + lastUpdated time.Time + + exhaustedStart time.Time + exhaustedMicros int64 +} + +// Init the token bucket. +func (tb *TokenBucket) Init(rate TokensPerSecond, burst Tokens) { + tb.InitWithNowFn(rate, burst, func() time.Time { + return time.Now() + }) +} + +// Init the token bucket with a custom "Now" function. +// Note that Wait/WaitCtx cannot be used with a custom time function. +func (tb *TokenBucket) InitWithNowFn(rate TokensPerSecond, burst Tokens, nowFn func() time.Time) { + *tb = TokenBucket{ + rate: rate, + burst: burst, + nowFn: nowFn, + current: burst, + lastUpdated: nowFn(), + } +} + +// Update moves the time forward, accounting for the replenishment since the +// last update. +func (tb *TokenBucket) Update() { + now := tb.nowFn() + if since := now.Sub(tb.lastUpdated); since > 0 { + tb.current += Tokens(float64(tb.rate) * since.Seconds()) + + if tb.current > tb.burst { + tb.current = tb.burst + } + tb.lastUpdated = now + tb.updateExhaustedMicros() + } +} + +// UpdateConfig updates the rate and burst limits. The change in burst will be +// applied to the current token quantity. For example, if the RateLimiter +// currently had 5 available tokens and the burst is updated from 10 to 20, the +// amount will increase to 15. Similarly, if the burst is decreased by 10, the +// current quota will decrease accordingly, potentially putting the limiter into +// debt. +func (tb *TokenBucket) UpdateConfig(rate TokensPerSecond, burst Tokens) { + tb.Update() + + burstDelta := burst - tb.burst + tb.rate = rate + tb.burst = burst + + tb.current += burstDelta + tb.updateExhaustedMicros() +} + +// Reset resets the current tokens to whatever the burst is. +func (tb *TokenBucket) Reset() { + tb.current = tb.burst + tb.updateExhaustedMicros() +} + +// Adjust returns tokens to the bucket (positive delta) or accounts for a debt +// of tokens (negative delta). +func (tb *TokenBucket) Adjust(delta Tokens) { + tb.Update() + tb.current += delta + if tb.current > tb.burst { + tb.current = tb.burst + } + tb.updateExhaustedMicros() +} + +// TryToFulfill either removes the given amount if is available, or returns a +// time after which the request should be retried. +func (tb *TokenBucket) TryToFulfill(amount Tokens) (fulfilled bool, tryAgainAfter time.Duration) { + tb.Update() + + // Deal with the case where the request is larger than the burst size. In + // this case we'll allow the acquisition to complete if and when the current + // value is equal to the burst. If the acquisition succeeds, it will put the + // limiter into debt. + want := amount + if want > tb.burst { + want = tb.burst + } + if delta := want - tb.current; delta > 0 { + // Compute the time it will take to get to the needed capacity. + timeDelta := time.Duration((float64(delta) * float64(time.Second)) / float64(tb.rate)) + if timeDelta < time.Nanosecond { + timeDelta = time.Nanosecond + } + return false, timeDelta + } + + tb.current -= amount + tb.updateExhaustedMicros() + return true, 0 +} + +// Wait removes the given amount, waiting as long as necessary. +func (tb *TokenBucket) Wait(amount Tokens) { + _ = tb.WaitCtx(context.Background(), amount) +} + +// WaitCtx removes the given amount, waiting as long as necessary or until the +// context is canceled. +func (tb *TokenBucket) WaitCtx(ctx context.Context, amount Tokens) error { + // We want to check for context cancelation even if we don't need to wait. + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + for { + fulfilled, tryAgainAfter := tb.TryToFulfill(amount) + if fulfilled { + return nil + } + select { + case <-time.After(tryAgainAfter): + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// Exhausted returns the cumulative duration over which this token bucket was +// exhausted. Exported only for metrics. +func (tb *TokenBucket) Exhausted() time.Duration { + var ongoingExhaustionMicros int64 + if !tb.exhaustedStart.IsZero() { + ongoingExhaustionMicros = tb.nowFn().Sub(tb.exhaustedStart).Microseconds() + } + return time.Duration(tb.exhaustedMicros+ongoingExhaustionMicros) * time.Microsecond +} + +// Available returns the currently available tokens (can be -ve). Exported only +// for metrics. +func (tb *TokenBucket) Available() Tokens { + return tb.current +} + +func (tb *TokenBucket) updateExhaustedMicros() { + now := tb.nowFn() + if tb.current <= 0 && tb.exhaustedStart.IsZero() { + tb.exhaustedStart = now + } + if tb.current > 0 && !tb.exhaustedStart.IsZero() { + tb.exhaustedMicros += now.Sub(tb.exhaustedStart).Microseconds() + tb.exhaustedStart = time.Time{} + } +} + +// TestingInternalParameters returns the refill rate (configured), burst tokens +// (configured), and number of available tokens where available <= burst. It's +// used in tests. +func (tb *TokenBucket) TestingInternalParameters() (rate TokensPerSecond, burst, available Tokens) { + tb.Update() + return tb.rate, tb.burst, tb.current +} diff --git a/vendor/github.com/getsentry/sentry-go/CHANGELOG.md b/vendor/github.com/getsentry/sentry-go/CHANGELOG.md index 303945b..4d26707 100644 --- a/vendor/github.com/getsentry/sentry-go/CHANGELOG.md +++ b/vendor/github.com/getsentry/sentry-go/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## 0.27.0 + +The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.27.0. + +### Breaking Changes + +- `Exception.ThreadId` is now typed as `uint64`. It was wrongly typed as `string` before. ([#770](https://github.com/getsentry/sentry-go/pull/770)) + +### Misc + +- Export `Event.Attachments` ([#771](https://github.com/getsentry/sentry-go/pull/771)) + ## 0.26.0 The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.26.0. diff --git a/vendor/github.com/getsentry/sentry-go/interfaces.go b/vendor/github.com/getsentry/sentry-go/interfaces.go index 13517f6..c769faa 100644 --- a/vendor/github.com/getsentry/sentry-go/interfaces.go +++ b/vendor/github.com/getsentry/sentry-go/interfaces.go @@ -241,7 +241,7 @@ type Exception struct { Type string `json:"type,omitempty"` // used as the main issue title Value string `json:"value,omitempty"` // used as the main issue subtitle Module string `json:"module,omitempty"` - ThreadID string `json:"thread_id,omitempty"` + ThreadID uint64 `json:"thread_id,omitempty"` Stacktrace *Stacktrace `json:"stacktrace,omitempty"` Mechanism *Mechanism `json:"mechanism,omitempty"` } @@ -317,6 +317,7 @@ type Event struct { Request *Request `json:"request,omitempty"` Exception []Exception `json:"exception,omitempty"` DebugMeta *DebugMeta `json:"debug_meta,omitempty"` + Attachments []*Attachment `json:"-"` // The fields below are only relevant for transactions. @@ -333,7 +334,6 @@ type Event struct { // The fields below are not part of the final JSON payload. sdkMetaData SDKMetaData - attachments []*Attachment } // SetException appends the unwrapped errors to the event's exception list. diff --git a/vendor/github.com/getsentry/sentry-go/scope.go b/vendor/github.com/getsentry/sentry-go/scope.go index 2f961ba..374010b 100644 --- a/vendor/github.com/getsentry/sentry-go/scope.go +++ b/vendor/github.com/getsentry/sentry-go/scope.go @@ -344,7 +344,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint) *Event { } if len(scope.attachments) > 0 { - event.attachments = append(event.attachments, scope.attachments...) + event.Attachments = append(event.Attachments, scope.attachments...) } if len(scope.tags) > 0 { diff --git a/vendor/github.com/getsentry/sentry-go/sentry.go b/vendor/github.com/getsentry/sentry-go/sentry.go index c44f906..3df2cdb 100644 --- a/vendor/github.com/getsentry/sentry-go/sentry.go +++ b/vendor/github.com/getsentry/sentry-go/sentry.go @@ -6,7 +6,7 @@ import ( ) // The version of the SDK. -const SDKVersion = "0.26.0" +const SDKVersion = "0.27.0" // apiVersion is the minimum version of the Sentry API compatible with the // sentry-go SDK. diff --git a/vendor/github.com/getsentry/sentry-go/transport.go b/vendor/github.com/getsentry/sentry-go/transport.go index 3eee458..0d03462 100644 --- a/vendor/github.com/getsentry/sentry-go/transport.go +++ b/vendor/github.com/getsentry/sentry-go/transport.go @@ -185,7 +185,7 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes } // Attachments - for _, attachment := range event.attachments { + for _, attachment := range event.Attachments { if err := encodeAttachment(enc, &b, attachment); err != nil { return nil, err } diff --git a/vendor/github.com/klauspost/compress/.goreleaser.yml b/vendor/github.com/klauspost/compress/.goreleaser.yml index 4c28dff..a229538 100644 --- a/vendor/github.com/klauspost/compress/.goreleaser.yml +++ b/vendor/github.com/klauspost/compress/.goreleaser.yml @@ -3,7 +3,6 @@ before: hooks: - ./gen.sh - - go install mvdan.cc/garble@v0.10.1 builds: - @@ -32,7 +31,6 @@ builds: - mips64le goarm: - 7 - gobinary: garble - id: "s2d" binary: s2d @@ -59,7 +57,6 @@ builds: - mips64le goarm: - 7 - gobinary: garble - id: "s2sx" binary: s2sx @@ -87,7 +84,6 @@ builds: - mips64le goarm: - 7 - gobinary: garble archives: - diff --git a/vendor/github.com/klauspost/compress/README.md b/vendor/github.com/klauspost/compress/README.md index 7e83f58..1f72cdd 100644 --- a/vendor/github.com/klauspost/compress/README.md +++ b/vendor/github.com/klauspost/compress/README.md @@ -16,6 +16,30 @@ This package provides various compression algorithms. # changelog +* Feb 5th, 2024 - [1.17.6](https://github.com/klauspost/compress/releases/tag/v1.17.6) + * zstd: Fix incorrect repeat coding in best mode https://github.com/klauspost/compress/pull/923 + * s2: Fix DecodeConcurrent deadlock on errors https://github.com/klauspost/compress/pull/925 + +* Jan 26th, 2024 - [v1.17.5](https://github.com/klauspost/compress/releases/tag/v1.17.5) + * flate: Fix reset with dictionary on custom window encodes https://github.com/klauspost/compress/pull/912 + * zstd: Add Frame header encoding and stripping https://github.com/klauspost/compress/pull/908 + * zstd: Limit better/best default window to 8MB https://github.com/klauspost/compress/pull/913 + * zstd: Speed improvements by @greatroar in https://github.com/klauspost/compress/pull/896 https://github.com/klauspost/compress/pull/910 + * s2: Fix callbacks for skippable blocks and disallow 0xfe (Padding) by @Jille in https://github.com/klauspost/compress/pull/916 https://github.com/klauspost/compress/pull/917 +https://github.com/klauspost/compress/pull/919 https://github.com/klauspost/compress/pull/918 + +* Dec 1st, 2023 - [v1.17.4](https://github.com/klauspost/compress/releases/tag/v1.17.4) + * huff0: Speed up symbol counting by @greatroar in https://github.com/klauspost/compress/pull/887 + * huff0: Remove byteReader by @greatroar in https://github.com/klauspost/compress/pull/886 + * gzhttp: Allow overriding decompression on transport https://github.com/klauspost/compress/pull/892 + * gzhttp: Clamp compression level https://github.com/klauspost/compress/pull/890 + * gzip: Error out if reserved bits are set https://github.com/klauspost/compress/pull/891 + +* Nov 15th, 2023 - [v1.17.3](https://github.com/klauspost/compress/releases/tag/v1.17.3) + * fse: Fix max header size https://github.com/klauspost/compress/pull/881 + * zstd: Improve better/best compression https://github.com/klauspost/compress/pull/877 + * gzhttp: Fix missing content type on Close https://github.com/klauspost/compress/pull/883 + * Oct 22nd, 2023 - [v1.17.2](https://github.com/klauspost/compress/releases/tag/v1.17.2) * zstd: Fix rare *CORRUPTION* output in "best" mode. See https://github.com/klauspost/compress/pull/876 @@ -554,7 +578,7 @@ For direct deflate use, NewStatelessWriter and StatelessDeflate are available. S A `bufio.Writer` can of course be used to control write sizes. For example, to use a 4KB buffer: -``` +```go // replace 'ioutil.Discard' with your output. gzw, err := gzip.NewWriterLevel(ioutil.Discard, gzip.StatelessCompression) if err != nil { diff --git a/vendor/github.com/klauspost/compress/s2sx.mod b/vendor/github.com/klauspost/compress/s2sx.mod index 2263853..5a4412f 100644 --- a/vendor/github.com/klauspost/compress/s2sx.mod +++ b/vendor/github.com/klauspost/compress/s2sx.mod @@ -1,4 +1,4 @@ module github.com/klauspost/compress -go 1.16 +go 1.19 diff --git a/vendor/github.com/klauspost/compress/zstd/decodeheader.go b/vendor/github.com/klauspost/compress/zstd/decodeheader.go index f6a2409..6a5a298 100644 --- a/vendor/github.com/klauspost/compress/zstd/decodeheader.go +++ b/vendor/github.com/klauspost/compress/zstd/decodeheader.go @@ -95,42 +95,54 @@ type Header struct { // If there isn't enough input, io.ErrUnexpectedEOF is returned. // The FirstBlock.OK will indicate if enough information was available to decode the first block header. func (h *Header) Decode(in []byte) error { + _, err := h.DecodeAndStrip(in) + return err +} + +// DecodeAndStrip will decode the header from the beginning of the stream +// and on success return the remaining bytes. +// This will decode the frame header and the first block header if enough bytes are provided. +// It is recommended to provide at least HeaderMaxSize bytes. +// If the frame header cannot be read an error will be returned. +// If there isn't enough input, io.ErrUnexpectedEOF is returned. +// The FirstBlock.OK will indicate if enough information was available to decode the first block header. +func (h *Header) DecodeAndStrip(in []byte) (remain []byte, err error) { *h = Header{} if len(in) < 4 { - return io.ErrUnexpectedEOF + return nil, io.ErrUnexpectedEOF } h.HeaderSize += 4 b, in := in[:4], in[4:] if string(b) != frameMagic { if string(b[1:4]) != skippableFrameMagic || b[0]&0xf0 != 0x50 { - return ErrMagicMismatch + return nil, ErrMagicMismatch } if len(in) < 4 { - return io.ErrUnexpectedEOF + return nil, io.ErrUnexpectedEOF } h.HeaderSize += 4 h.Skippable = true h.SkippableID = int(b[0] & 0xf) h.SkippableSize = binary.LittleEndian.Uint32(in) - return nil + return in[4:], nil } // Read Window_Descriptor // https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md#window_descriptor if len(in) < 1 { - return io.ErrUnexpectedEOF + return nil, io.ErrUnexpectedEOF } fhd, in := in[0], in[1:] h.HeaderSize++ h.SingleSegment = fhd&(1<<5) != 0 h.HasCheckSum = fhd&(1<<2) != 0 if fhd&(1<<3) != 0 { - return errors.New("reserved bit set on frame header") + return nil, errors.New("reserved bit set on frame header") } if !h.SingleSegment { if len(in) < 1 { - return io.ErrUnexpectedEOF + return nil, io.ErrUnexpectedEOF } var wd byte wd, in = in[0], in[1:] @@ -148,7 +160,7 @@ func (h *Header) Decode(in []byte) error { size = 4 } if len(in) < int(size) { - return io.ErrUnexpectedEOF + return nil, io.ErrUnexpectedEOF } b, in = in[:size], in[size:] h.HeaderSize += int(size) @@ -178,7 +190,7 @@ func (h *Header) Decode(in []byte) error { if fcsSize > 0 { h.HasFCS = true if len(in) < fcsSize { - return io.ErrUnexpectedEOF + return nil, io.ErrUnexpectedEOF } b, in = in[:fcsSize], in[fcsSize:] h.HeaderSize += int(fcsSize) @@ -199,7 +211,7 @@ func (h *Header) Decode(in []byte) error { // Frame Header done, we will not fail from now on. if len(in) < 3 { - return nil + return in, nil } tmp := in[:3] bh := uint32(tmp[0]) | (uint32(tmp[1]) << 8) | (uint32(tmp[2]) << 16) @@ -209,7 +221,7 @@ func (h *Header) Decode(in []byte) error { cSize := int(bh >> 3) switch blockType { case blockTypeReserved: - return nil + return in, nil case blockTypeRLE: h.FirstBlock.Compressed = true h.FirstBlock.DecompressedSize = cSize @@ -225,5 +237,25 @@ func (h *Header) Decode(in []byte) error { } h.FirstBlock.OK = true - return nil + return in, nil +} + +// AppendTo will append the encoded header to the dst slice. +// There is no error checking performed on the header values. +func (h *Header) AppendTo(dst []byte) ([]byte, error) { + if h.Skippable { + magic := [4]byte{0x50, 0x2a, 0x4d, 0x18} + magic[0] |= byte(h.SkippableID & 0xf) + dst = append(dst, magic[:]...) + f := h.SkippableSize + return append(dst, uint8(f), uint8(f>>8), uint8(f>>16), uint8(f>>24)), nil + } + f := frameHeader{ + ContentSize: h.FrameContentSize, + WindowSize: uint32(h.WindowSize), + SingleSegment: h.SingleSegment, + Checksum: h.HasCheckSum, + DictID: h.DictionaryID, + } + return f.appendTo(dst), nil } diff --git a/vendor/github.com/klauspost/compress/zstd/enc_best.go b/vendor/github.com/klauspost/compress/zstd/enc_best.go index c81a153..87f4287 100644 --- a/vendor/github.com/klauspost/compress/zstd/enc_best.go +++ b/vendor/github.com/klauspost/compress/zstd/enc_best.go @@ -201,14 +201,6 @@ encodeLoop: if delta >= e.maxMatchOff || delta <= 0 || load3232(src, offset) != first { return } - if debugAsserts { - if offset >= s { - panic(fmt.Sprintf("offset: %d - s:%d - rep: %d - cur :%d - max: %d", offset, s, rep, e.cur, e.maxMatchOff)) - } - if !bytes.Equal(src[s:s+4], src[offset:offset+4]) { - panic(fmt.Sprintf("first match mismatch: %v != %v, first: %08x", src[s:s+4], src[offset:offset+4], first)) - } - } // Try to quick reject if we already have a long match. if m.length > 16 { left := len(src) - int(m.s+m.length) @@ -227,8 +219,10 @@ encodeLoop: } } l := 4 + e.matchlen(s+4, offset+4, src) - if true { + if m.rep <= 0 { // Extend candidate match backwards as far as possible. + // Do not extend repeats as we can assume they are optimal + // and offsets change if s == nextEmit. tMin := s - e.maxMatchOff if tMin < 0 { tMin = 0 @@ -239,7 +233,14 @@ encodeLoop: l++ } } - + if debugAsserts { + if offset >= s { + panic(fmt.Sprintf("offset: %d - s:%d - rep: %d - cur :%d - max: %d", offset, s, rep, e.cur, e.maxMatchOff)) + } + if !bytes.Equal(src[s:s+l], src[offset:offset+l]) { + panic(fmt.Sprintf("second match mismatch: %v != %v, first: %08x", src[s:s+4], src[offset:offset+4], first)) + } + } cand := match{offset: offset, s: s, length: l, rep: rep} cand.estBits(bitsPerByte) if m.est >= highScore || cand.est-m.est+(cand.s-m.s)*bitsPerByte>>10 < 0 { @@ -336,24 +337,31 @@ encodeLoop: } if debugAsserts { + if best.offset >= best.s { + panic(fmt.Sprintf("best.offset > s: %d >= %d", best.offset, best.s)) + } + if best.s < nextEmit { + panic(fmt.Sprintf("s %d < nextEmit %d", best.s, nextEmit)) + } + if best.offset < s-e.maxMatchOff { + panic(fmt.Sprintf("best.offset < s-e.maxMatchOff: %d < %d", best.offset, s-e.maxMatchOff)) + } if !bytes.Equal(src[best.s:best.s+best.length], src[best.offset:best.offset+best.length]) { panic(fmt.Sprintf("match mismatch: %v != %v", src[best.s:best.s+best.length], src[best.offset:best.offset+best.length])) } } // We have a match, we can store the forward value + s = best.s if best.rep > 0 { var seq seq seq.matchLen = uint32(best.length - zstdMinMatch) - if debugAsserts && s < nextEmit { - panic("s < nextEmit") - } addLiterals(&seq, best.s) // Repeat. If bit 4 is set, this is a non-lit repeat. seq.offset = uint32(best.rep & 3) if debugSequences { - println("repeat sequence", seq, "next s:", s) + println("repeat sequence", seq, "next s:", best.s, "off:", best.s-best.offset) } blk.sequences = append(blk.sequences, seq) @@ -396,7 +404,6 @@ encodeLoop: // A 4-byte match has been found. Update recent offsets. // We'll later see if more than 4 bytes. - s = best.s t := best.offset offset1, offset2, offset3 = s-t, offset1, offset2 diff --git a/vendor/github.com/klauspost/compress/zstd/encoder_options.go b/vendor/github.com/klauspost/compress/zstd/encoder_options.go index faaf819..20671dc 100644 --- a/vendor/github.com/klauspost/compress/zstd/encoder_options.go +++ b/vendor/github.com/klauspost/compress/zstd/encoder_options.go @@ -94,7 +94,7 @@ func WithEncoderConcurrency(n int) EOption { // The value must be a power of two between MinWindowSize and MaxWindowSize. // A larger value will enable better compression but allocate more memory and, // for above-default values, take considerably longer. -// The default value is determined by the compression level. +// The default value is determined by the compression level and max 8MB. func WithWindowSize(n int) EOption { return func(o *encoderOptions) error { switch { @@ -232,9 +232,9 @@ func WithEncoderLevel(l EncoderLevel) EOption { case SpeedDefault: o.windowSize = 8 << 20 case SpeedBetterCompression: - o.windowSize = 16 << 20 + o.windowSize = 8 << 20 case SpeedBestCompression: - o.windowSize = 32 << 20 + o.windowSize = 8 << 20 } } if !o.customALEntropy { diff --git a/vendor/github.com/klauspost/compress/zstd/frameenc.go b/vendor/github.com/klauspost/compress/zstd/frameenc.go index 2f5d5ed..667ca06 100644 --- a/vendor/github.com/klauspost/compress/zstd/frameenc.go +++ b/vendor/github.com/klauspost/compress/zstd/frameenc.go @@ -76,7 +76,7 @@ func (f frameHeader) appendTo(dst []byte) []byte { if f.SingleSegment { dst = append(dst, uint8(f.ContentSize)) } - // Unless SingleSegment is set, framessizes < 256 are nto stored. + // Unless SingleSegment is set, framessizes < 256 are not stored. case 1: f.ContentSize -= 256 dst = append(dst, uint8(f.ContentSize), uint8(f.ContentSize>>8)) diff --git a/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go b/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go index 332e51f..8adfebb 100644 --- a/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go +++ b/vendor/github.com/klauspost/compress/zstd/fse_decoder_generic.go @@ -20,10 +20,9 @@ func (s *fseDecoder) buildDtable() error { if v == -1 { s.dt[highThreshold].setAddBits(uint8(i)) highThreshold-- - symbolNext[i] = 1 - } else { - symbolNext[i] = uint16(v) + v = 1 } + symbolNext[i] = uint16(v) } } @@ -35,10 +34,12 @@ func (s *fseDecoder) buildDtable() error { for ss, v := range s.norm[:s.symbolLen] { for i := 0; i < int(v); i++ { s.dt[position].setAddBits(uint8(ss)) - position = (position + step) & tableMask - for position > highThreshold { + for { // lowprob area position = (position + step) & tableMask + if position <= highThreshold { + break + } } } } diff --git a/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s b/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s index 974b997..5b06174 100644 --- a/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s +++ b/vendor/github.com/klauspost/compress/zstd/seqdec_amd64.s @@ -157,8 +157,7 @@ sequenceDecs_decode_amd64_ll_update_zero: // Update Literal Length State MOVBQZX DI, R14 - SHRQ $0x10, DI - MOVWQZX DI, DI + SHRL $0x10, DI LEAQ (BX)(R14*1), CX MOVQ DX, R15 MOVQ CX, BX @@ -177,8 +176,7 @@ sequenceDecs_decode_amd64_ll_update_zero: // Update Match Length State MOVBQZX R8, R14 - SHRQ $0x10, R8 - MOVWQZX R8, R8 + SHRL $0x10, R8 LEAQ (BX)(R14*1), CX MOVQ DX, R15 MOVQ CX, BX @@ -197,8 +195,7 @@ sequenceDecs_decode_amd64_ll_update_zero: // Update Offset State MOVBQZX R9, R14 - SHRQ $0x10, R9 - MOVWQZX R9, R9 + SHRL $0x10, R9 LEAQ (BX)(R14*1), CX MOVQ DX, R15 MOVQ CX, BX @@ -459,8 +456,7 @@ sequenceDecs_decode_56_amd64_ll_update_zero: // Update Literal Length State MOVBQZX DI, R14 - SHRQ $0x10, DI - MOVWQZX DI, DI + SHRL $0x10, DI LEAQ (BX)(R14*1), CX MOVQ DX, R15 MOVQ CX, BX @@ -479,8 +475,7 @@ sequenceDecs_decode_56_amd64_ll_update_zero: // Update Match Length State MOVBQZX R8, R14 - SHRQ $0x10, R8 - MOVWQZX R8, R8 + SHRL $0x10, R8 LEAQ (BX)(R14*1), CX MOVQ DX, R15 MOVQ CX, BX @@ -499,8 +494,7 @@ sequenceDecs_decode_56_amd64_ll_update_zero: // Update Offset State MOVBQZX R9, R14 - SHRQ $0x10, R9 - MOVWQZX R9, R9 + SHRL $0x10, R9 LEAQ (BX)(R14*1), CX MOVQ DX, R15 MOVQ CX, BX @@ -772,11 +766,10 @@ sequenceDecs_decode_bmi2_fill_2_end: BZHIQ R14, R15, R15 // Update Offset State - BZHIQ R8, R15, CX - SHRXQ R8, R15, R15 - MOVQ $0x00001010, R14 - BEXTRQ R14, R8, R8 - ADDQ CX, R8 + BZHIQ R8, R15, CX + SHRXQ R8, R15, R15 + SHRL $0x10, R8 + ADDQ CX, R8 // Load ctx.ofTable MOVQ ctx+16(FP), CX @@ -784,11 +777,10 @@ sequenceDecs_decode_bmi2_fill_2_end: MOVQ (CX)(R8*8), R8 // Update Match Length State - BZHIQ DI, R15, CX - SHRXQ DI, R15, R15 - MOVQ $0x00001010, R14 - BEXTRQ R14, DI, DI - ADDQ CX, DI + BZHIQ DI, R15, CX + SHRXQ DI, R15, R15 + SHRL $0x10, DI + ADDQ CX, DI // Load ctx.mlTable MOVQ ctx+16(FP), CX @@ -796,10 +788,9 @@ sequenceDecs_decode_bmi2_fill_2_end: MOVQ (CX)(DI*8), DI // Update Literal Length State - BZHIQ SI, R15, CX - MOVQ $0x00001010, R14 - BEXTRQ R14, SI, SI - ADDQ CX, SI + BZHIQ SI, R15, CX + SHRL $0x10, SI + ADDQ CX, SI // Load ctx.llTable MOVQ ctx+16(FP), CX @@ -1032,11 +1023,10 @@ sequenceDecs_decode_56_bmi2_fill_end: BZHIQ R14, R15, R15 // Update Offset State - BZHIQ R8, R15, CX - SHRXQ R8, R15, R15 - MOVQ $0x00001010, R14 - BEXTRQ R14, R8, R8 - ADDQ CX, R8 + BZHIQ R8, R15, CX + SHRXQ R8, R15, R15 + SHRL $0x10, R8 + ADDQ CX, R8 // Load ctx.ofTable MOVQ ctx+16(FP), CX @@ -1044,11 +1034,10 @@ sequenceDecs_decode_56_bmi2_fill_end: MOVQ (CX)(R8*8), R8 // Update Match Length State - BZHIQ DI, R15, CX - SHRXQ DI, R15, R15 - MOVQ $0x00001010, R14 - BEXTRQ R14, DI, DI - ADDQ CX, DI + BZHIQ DI, R15, CX + SHRXQ DI, R15, R15 + SHRL $0x10, DI + ADDQ CX, DI // Load ctx.mlTable MOVQ ctx+16(FP), CX @@ -1056,10 +1045,9 @@ sequenceDecs_decode_56_bmi2_fill_end: MOVQ (CX)(DI*8), DI // Update Literal Length State - BZHIQ SI, R15, CX - MOVQ $0x00001010, R14 - BEXTRQ R14, SI, SI - ADDQ CX, SI + BZHIQ SI, R15, CX + SHRL $0x10, SI + ADDQ CX, SI // Load ctx.llTable MOVQ ctx+16(FP), CX @@ -1967,8 +1955,7 @@ sequenceDecs_decodeSync_amd64_ll_update_zero: // Update Literal Length State MOVBQZX DI, R13 - SHRQ $0x10, DI - MOVWQZX DI, DI + SHRL $0x10, DI LEAQ (BX)(R13*1), CX MOVQ DX, R14 MOVQ CX, BX @@ -1987,8 +1974,7 @@ sequenceDecs_decodeSync_amd64_ll_update_zero: // Update Match Length State MOVBQZX R8, R13 - SHRQ $0x10, R8 - MOVWQZX R8, R8 + SHRL $0x10, R8 LEAQ (BX)(R13*1), CX MOVQ DX, R14 MOVQ CX, BX @@ -2007,8 +1993,7 @@ sequenceDecs_decodeSync_amd64_ll_update_zero: // Update Offset State MOVBQZX R9, R13 - SHRQ $0x10, R9 - MOVWQZX R9, R9 + SHRL $0x10, R9 LEAQ (BX)(R13*1), CX MOVQ DX, R14 MOVQ CX, BX @@ -2514,11 +2499,10 @@ sequenceDecs_decodeSync_bmi2_fill_2_end: BZHIQ R13, R14, R14 // Update Offset State - BZHIQ R8, R14, CX - SHRXQ R8, R14, R14 - MOVQ $0x00001010, R13 - BEXTRQ R13, R8, R8 - ADDQ CX, R8 + BZHIQ R8, R14, CX + SHRXQ R8, R14, R14 + SHRL $0x10, R8 + ADDQ CX, R8 // Load ctx.ofTable MOVQ ctx+16(FP), CX @@ -2526,11 +2510,10 @@ sequenceDecs_decodeSync_bmi2_fill_2_end: MOVQ (CX)(R8*8), R8 // Update Match Length State - BZHIQ DI, R14, CX - SHRXQ DI, R14, R14 - MOVQ $0x00001010, R13 - BEXTRQ R13, DI, DI - ADDQ CX, DI + BZHIQ DI, R14, CX + SHRXQ DI, R14, R14 + SHRL $0x10, DI + ADDQ CX, DI // Load ctx.mlTable MOVQ ctx+16(FP), CX @@ -2538,10 +2521,9 @@ sequenceDecs_decodeSync_bmi2_fill_2_end: MOVQ (CX)(DI*8), DI // Update Literal Length State - BZHIQ SI, R14, CX - MOVQ $0x00001010, R13 - BEXTRQ R13, SI, SI - ADDQ CX, SI + BZHIQ SI, R14, CX + SHRL $0x10, SI + ADDQ CX, SI // Load ctx.llTable MOVQ ctx+16(FP), CX @@ -3055,8 +3037,7 @@ sequenceDecs_decodeSync_safe_amd64_ll_update_zero: // Update Literal Length State MOVBQZX DI, R13 - SHRQ $0x10, DI - MOVWQZX DI, DI + SHRL $0x10, DI LEAQ (BX)(R13*1), CX MOVQ DX, R14 MOVQ CX, BX @@ -3075,8 +3056,7 @@ sequenceDecs_decodeSync_safe_amd64_ll_update_zero: // Update Match Length State MOVBQZX R8, R13 - SHRQ $0x10, R8 - MOVWQZX R8, R8 + SHRL $0x10, R8 LEAQ (BX)(R13*1), CX MOVQ DX, R14 MOVQ CX, BX @@ -3095,8 +3075,7 @@ sequenceDecs_decodeSync_safe_amd64_ll_update_zero: // Update Offset State MOVBQZX R9, R13 - SHRQ $0x10, R9 - MOVWQZX R9, R9 + SHRL $0x10, R9 LEAQ (BX)(R13*1), CX MOVQ DX, R14 MOVQ CX, BX @@ -3704,11 +3683,10 @@ sequenceDecs_decodeSync_safe_bmi2_fill_2_end: BZHIQ R13, R14, R14 // Update Offset State - BZHIQ R8, R14, CX - SHRXQ R8, R14, R14 - MOVQ $0x00001010, R13 - BEXTRQ R13, R8, R8 - ADDQ CX, R8 + BZHIQ R8, R14, CX + SHRXQ R8, R14, R14 + SHRL $0x10, R8 + ADDQ CX, R8 // Load ctx.ofTable MOVQ ctx+16(FP), CX @@ -3716,11 +3694,10 @@ sequenceDecs_decodeSync_safe_bmi2_fill_2_end: MOVQ (CX)(R8*8), R8 // Update Match Length State - BZHIQ DI, R14, CX - SHRXQ DI, R14, R14 - MOVQ $0x00001010, R13 - BEXTRQ R13, DI, DI - ADDQ CX, DI + BZHIQ DI, R14, CX + SHRXQ DI, R14, R14 + SHRL $0x10, DI + ADDQ CX, DI // Load ctx.mlTable MOVQ ctx+16(FP), CX @@ -3728,10 +3705,9 @@ sequenceDecs_decodeSync_safe_bmi2_fill_2_end: MOVQ (CX)(DI*8), DI // Update Literal Length State - BZHIQ SI, R14, CX - MOVQ $0x00001010, R13 - BEXTRQ R13, SI, SI - ADDQ CX, SI + BZHIQ SI, R14, CX + SHRL $0x10, SI + ADDQ CX, SI // Load ctx.llTable MOVQ ctx+16(FP), CX diff --git a/vendor/github.com/prometheus/client_model/go/metrics.pb.go b/vendor/github.com/prometheus/client_model/go/metrics.pb.go index cee360d..2f15490 100644 --- a/vendor/github.com/prometheus/client_model/go/metrics.pb.go +++ b/vendor/github.com/prometheus/client_model/go/metrics.pb.go @@ -483,6 +483,8 @@ type Histogram struct { // histograms. PositiveDelta []int64 `protobuf:"zigzag64,13,rep,name=positive_delta,json=positiveDelta" json:"positive_delta,omitempty"` // Count delta of each bucket compared to previous one (or to zero for 1st bucket). PositiveCount []float64 `protobuf:"fixed64,14,rep,name=positive_count,json=positiveCount" json:"positive_count,omitempty"` // Absolute count of each bucket. + // Only used for native histograms. These exemplars MUST have a timestamp. + Exemplars []*Exemplar `protobuf:"bytes,16,rep,name=exemplars" json:"exemplars,omitempty"` } func (x *Histogram) Reset() { @@ -622,6 +624,13 @@ func (x *Histogram) GetPositiveCount() []float64 { return nil } +func (x *Histogram) GetExemplars() []*Exemplar { + if x != nil { + return x.Exemplars + } + return nil +} + // A Bucket of a conventional histogram, each of which is treated as // an individual counter-like time series by Prometheus. type Bucket struct { @@ -923,6 +932,7 @@ type MetricFamily struct { Help *string `protobuf:"bytes,2,opt,name=help" json:"help,omitempty"` Type *MetricType `protobuf:"varint,3,opt,name=type,enum=io.prometheus.client.MetricType" json:"type,omitempty"` Metric []*Metric `protobuf:"bytes,4,rep,name=metric" json:"metric,omitempty"` + Unit *string `protobuf:"bytes,5,opt,name=unit" json:"unit,omitempty"` } func (x *MetricFamily) Reset() { @@ -985,6 +995,13 @@ func (x *MetricFamily) GetMetric() []*Metric { return nil } +func (x *MetricFamily) GetUnit() string { + if x != nil && x.Unit != nil { + return *x.Unit + } + return "" +} + var File_io_prometheus_client_metrics_proto protoreflect.FileDescriptor var file_io_prometheus_client_metrics_proto_rawDesc = []byte{ @@ -1028,7 +1045,7 @@ var file_io_prometheus_client_metrics_proto_rawDesc = []byte{ 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x10, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x1f, 0x0a, 0x07, 0x55, 0x6e, 0x74, 0x79, 0x70, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xac, 0x05, 0x0a, 0x09, 0x48, + 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0xea, 0x05, 0x0a, 0x09, 0x48, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x73, @@ -1071,79 +1088,84 @@ var file_io_prometheus_client_metrics_proto_rawDesc = []byte{ 0x03, 0x28, 0x12, 0x52, 0x0d, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x44, 0x65, 0x6c, 0x74, 0x61, 0x12, 0x25, 0x0a, 0x0e, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0e, 0x20, 0x03, 0x28, 0x01, 0x52, 0x0d, 0x70, 0x6f, 0x73, 0x69, - 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc6, 0x01, 0x0a, 0x06, 0x42, 0x75, - 0x63, 0x6b, 0x65, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, - 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, - 0x63, 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x34, 0x0a, 0x16, 0x63, 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x5f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, - 0x14, 0x63, 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x46, 0x6c, 0x6f, 0x61, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x70, 0x70, 0x65, 0x72, 0x5f, 0x62, - 0x6f, 0x75, 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0a, 0x75, 0x70, 0x70, 0x65, - 0x72, 0x42, 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x3a, 0x0a, 0x08, 0x65, 0x78, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, - 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, - 0x45, 0x78, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, 0x52, 0x08, 0x65, 0x78, 0x65, 0x6d, 0x70, 0x6c, - 0x61, 0x72, 0x22, 0x3c, 0x0a, 0x0a, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x70, 0x61, 0x6e, - 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x11, - 0x52, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, - 0x74, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, - 0x22, 0x91, 0x01, 0x0a, 0x08, 0x45, 0x78, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, 0x12, 0x35, 0x0a, + 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x3c, 0x0a, 0x09, 0x65, 0x78, 0x65, + 0x6d, 0x70, 0x6c, 0x61, 0x72, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x69, + 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, + 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x78, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, 0x52, 0x09, 0x65, 0x78, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, 0x73, 0x22, 0xc6, 0x01, 0x0a, 0x06, 0x42, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x63, 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x63, 0x75, + 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x34, 0x0a, + 0x16, 0x63, 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x5f, 0x66, 0x6c, 0x6f, 0x61, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x01, 0x52, 0x14, 0x63, + 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x46, 0x6c, + 0x6f, 0x61, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x75, 0x70, 0x70, 0x65, 0x72, 0x5f, 0x62, 0x6f, 0x75, + 0x6e, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x52, 0x0a, 0x75, 0x70, 0x70, 0x65, 0x72, 0x42, + 0x6f, 0x75, 0x6e, 0x64, 0x12, 0x3a, 0x0a, 0x08, 0x65, 0x78, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, + 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x78, + 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, 0x52, 0x08, 0x65, 0x78, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, + 0x22, 0x3c, 0x0a, 0x0a, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x70, 0x61, 0x6e, 0x12, 0x16, + 0x0a, 0x06, 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x11, 0x52, 0x06, + 0x6f, 0x66, 0x66, 0x73, 0x65, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x22, 0x91, + 0x01, 0x0a, 0x08, 0x45, 0x78, 0x65, 0x6d, 0x70, 0x6c, 0x61, 0x72, 0x12, 0x35, 0x0a, 0x05, 0x6c, + 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x6f, 0x2e, + 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, + 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x50, 0x61, 0x69, 0x72, 0x52, 0x05, 0x6c, 0x61, 0x62, + 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, + 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x22, 0xff, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x35, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x50, 0x61, 0x69, 0x72, 0x52, 0x05, 0x6c, - 0x61, 0x62, 0x65, 0x6c, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, - 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, - 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, - 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x22, 0xff, 0x02, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, - 0x35, 0x0a, 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, - 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x50, 0x61, 0x69, 0x72, 0x52, - 0x05, 0x6c, 0x61, 0x62, 0x65, 0x6c, 0x12, 0x31, 0x0a, 0x05, 0x67, 0x61, 0x75, 0x67, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, - 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x61, 0x75, - 0x67, 0x65, 0x52, 0x05, 0x67, 0x61, 0x75, 0x67, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6f, 0x2e, + 0x61, 0x62, 0x65, 0x6c, 0x12, 0x31, 0x0a, 0x05, 0x67, 0x61, 0x75, 0x67, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, + 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x47, 0x61, 0x75, 0x67, 0x65, + 0x52, 0x05, 0x67, 0x61, 0x75, 0x67, 0x65, 0x12, 0x37, 0x0a, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x65, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, + 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, + 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, + 0x12, 0x37, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, + 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, + 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x37, 0x0a, 0x07, 0x75, 0x6e, 0x74, + 0x79, 0x70, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, - 0x74, 0x2e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x65, 0x72, 0x52, 0x07, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x65, 0x72, 0x12, 0x37, 0x0a, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, - 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x53, 0x75, 0x6d, 0x6d, 0x61, - 0x72, 0x79, 0x52, 0x07, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x37, 0x0a, 0x07, 0x75, - 0x6e, 0x74, 0x79, 0x70, 0x65, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x69, - 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, 0x2e, 0x55, 0x6e, 0x74, 0x79, 0x70, 0x65, 0x64, 0x52, 0x07, 0x75, 0x6e, 0x74, - 0x79, 0x70, 0x65, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, - 0x6d, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, - 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x48, - 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x52, 0x09, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x67, - 0x72, 0x61, 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, - 0x5f, 0x6d, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x4d, 0x73, 0x22, 0xa2, 0x01, 0x0a, 0x0c, 0x4d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x46, 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, - 0x65, 0x6c, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x65, 0x6c, 0x70, 0x12, - 0x34, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, - 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, - 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x52, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x34, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, - 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, - 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, - 0x72, 0x69, 0x63, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x2a, 0x62, 0x0a, 0x0a, 0x4d, - 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x43, 0x4f, 0x55, - 0x4e, 0x54, 0x45, 0x52, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x41, 0x55, 0x47, 0x45, 0x10, - 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x4d, 0x4d, 0x41, 0x52, 0x59, 0x10, 0x02, 0x12, 0x0b, - 0x0a, 0x07, 0x55, 0x4e, 0x54, 0x59, 0x50, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0d, 0x0a, 0x09, 0x48, - 0x49, 0x53, 0x54, 0x4f, 0x47, 0x52, 0x41, 0x4d, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x47, 0x41, - 0x55, 0x47, 0x45, 0x5f, 0x48, 0x49, 0x53, 0x54, 0x4f, 0x47, 0x52, 0x41, 0x4d, 0x10, 0x05, 0x42, - 0x52, 0x0a, 0x14, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, - 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2f, 0x63, - 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2f, 0x67, 0x6f, 0x3b, 0x69, - 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x5f, 0x63, 0x6c, 0x69, - 0x65, 0x6e, 0x74, + 0x74, 0x2e, 0x55, 0x6e, 0x74, 0x79, 0x70, 0x65, 0x64, 0x52, 0x07, 0x75, 0x6e, 0x74, 0x79, 0x70, + 0x65, 0x64, 0x12, 0x3d, 0x0a, 0x09, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x18, + 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, + 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x48, 0x69, 0x73, + 0x74, 0x6f, 0x67, 0x72, 0x61, 0x6d, 0x52, 0x09, 0x68, 0x69, 0x73, 0x74, 0x6f, 0x67, 0x72, 0x61, + 0x6d, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x5f, 0x6d, + 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, + 0x6d, 0x70, 0x4d, 0x73, 0x22, 0xb6, 0x01, 0x0a, 0x0c, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x46, + 0x61, 0x6d, 0x69, 0x6c, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x65, 0x6c, + 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x65, 0x6c, 0x70, 0x12, 0x34, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x69, 0x6f, + 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, + 0x79, 0x70, 0x65, 0x12, 0x34, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, + 0x65, 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x52, 0x06, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x12, 0x0a, 0x04, 0x75, 0x6e, 0x69, + 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x75, 0x6e, 0x69, 0x74, 0x2a, 0x62, 0x0a, + 0x0a, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b, 0x0a, 0x07, 0x43, + 0x4f, 0x55, 0x4e, 0x54, 0x45, 0x52, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, 0x47, 0x41, 0x55, 0x47, + 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x53, 0x55, 0x4d, 0x4d, 0x41, 0x52, 0x59, 0x10, 0x02, + 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x54, 0x59, 0x50, 0x45, 0x44, 0x10, 0x03, 0x12, 0x0d, 0x0a, + 0x09, 0x48, 0x49, 0x53, 0x54, 0x4f, 0x47, 0x52, 0x41, 0x4d, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, + 0x47, 0x41, 0x55, 0x47, 0x45, 0x5f, 0x48, 0x49, 0x53, 0x54, 0x4f, 0x47, 0x52, 0x41, 0x4d, 0x10, + 0x05, 0x42, 0x52, 0x0a, 0x14, 0x69, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, + 0x75, 0x73, 0x2e, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, + 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, + 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x5f, 0x6d, 0x6f, 0x64, 0x65, 0x6c, 0x2f, 0x67, 0x6f, + 0x3b, 0x69, 0x6f, 0x5f, 0x70, 0x72, 0x6f, 0x6d, 0x65, 0x74, 0x68, 0x65, 0x75, 0x73, 0x5f, 0x63, + 0x6c, 0x69, 0x65, 0x6e, 0x74, } var ( @@ -1185,22 +1207,23 @@ var file_io_prometheus_client_metrics_proto_depIdxs = []int32{ 13, // 5: io.prometheus.client.Histogram.created_timestamp:type_name -> google.protobuf.Timestamp 9, // 6: io.prometheus.client.Histogram.negative_span:type_name -> io.prometheus.client.BucketSpan 9, // 7: io.prometheus.client.Histogram.positive_span:type_name -> io.prometheus.client.BucketSpan - 10, // 8: io.prometheus.client.Bucket.exemplar:type_name -> io.prometheus.client.Exemplar - 1, // 9: io.prometheus.client.Exemplar.label:type_name -> io.prometheus.client.LabelPair - 13, // 10: io.prometheus.client.Exemplar.timestamp:type_name -> google.protobuf.Timestamp - 1, // 11: io.prometheus.client.Metric.label:type_name -> io.prometheus.client.LabelPair - 2, // 12: io.prometheus.client.Metric.gauge:type_name -> io.prometheus.client.Gauge - 3, // 13: io.prometheus.client.Metric.counter:type_name -> io.prometheus.client.Counter - 5, // 14: io.prometheus.client.Metric.summary:type_name -> io.prometheus.client.Summary - 6, // 15: io.prometheus.client.Metric.untyped:type_name -> io.prometheus.client.Untyped - 7, // 16: io.prometheus.client.Metric.histogram:type_name -> io.prometheus.client.Histogram - 0, // 17: io.prometheus.client.MetricFamily.type:type_name -> io.prometheus.client.MetricType - 11, // 18: io.prometheus.client.MetricFamily.metric:type_name -> io.prometheus.client.Metric - 19, // [19:19] is the sub-list for method output_type - 19, // [19:19] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 10, // 8: io.prometheus.client.Histogram.exemplars:type_name -> io.prometheus.client.Exemplar + 10, // 9: io.prometheus.client.Bucket.exemplar:type_name -> io.prometheus.client.Exemplar + 1, // 10: io.prometheus.client.Exemplar.label:type_name -> io.prometheus.client.LabelPair + 13, // 11: io.prometheus.client.Exemplar.timestamp:type_name -> google.protobuf.Timestamp + 1, // 12: io.prometheus.client.Metric.label:type_name -> io.prometheus.client.LabelPair + 2, // 13: io.prometheus.client.Metric.gauge:type_name -> io.prometheus.client.Gauge + 3, // 14: io.prometheus.client.Metric.counter:type_name -> io.prometheus.client.Counter + 5, // 15: io.prometheus.client.Metric.summary:type_name -> io.prometheus.client.Summary + 6, // 16: io.prometheus.client.Metric.untyped:type_name -> io.prometheus.client.Untyped + 7, // 17: io.prometheus.client.Metric.histogram:type_name -> io.prometheus.client.Histogram + 0, // 18: io.prometheus.client.MetricFamily.type:type_name -> io.prometheus.client.MetricType + 11, // 19: io.prometheus.client.MetricFamily.metric:type_name -> io.prometheus.client.Metric + 20, // [20:20] is the sub-list for method output_type + 20, // [20:20] is the sub-list for method input_type + 20, // [20:20] is the sub-list for extension type_name + 20, // [20:20] is the sub-list for extension extendee + 0, // [0:20] is the sub-list for field type_name } func init() { file_io_prometheus_client_metrics_proto_init() } diff --git a/vendor/github.com/prometheus/common/expfmt/decode.go b/vendor/github.com/prometheus/common/expfmt/decode.go index a909b17..b2b89b0 100644 --- a/vendor/github.com/prometheus/common/expfmt/decode.go +++ b/vendor/github.com/prometheus/common/expfmt/decode.go @@ -45,7 +45,7 @@ func ResponseFormat(h http.Header) Format { mediatype, params, err := mime.ParseMediaType(ct) if err != nil { - return FmtUnknown + return fmtUnknown } const textType = "text/plain" @@ -53,28 +53,28 @@ func ResponseFormat(h http.Header) Format { switch mediatype { case ProtoType: if p, ok := params["proto"]; ok && p != ProtoProtocol { - return FmtUnknown + return fmtUnknown } if e, ok := params["encoding"]; ok && e != "delimited" { - return FmtUnknown + return fmtUnknown } - return FmtProtoDelim + return fmtProtoDelim case textType: if v, ok := params["version"]; ok && v != TextVersion { - return FmtUnknown + return fmtUnknown } - return FmtText + return fmtText } - return FmtUnknown + return fmtUnknown } // NewDecoder returns a new decoder based on the given input format. // If the input format does not imply otherwise, a text format decoder is returned. func NewDecoder(r io.Reader, format Format) Decoder { - switch format { - case FmtProtoDelim: + switch format.FormatType() { + case TypeProtoDelim: return &protoDecoder{r: r} } return &textDecoder{r: r} diff --git a/vendor/github.com/prometheus/common/expfmt/encode.go b/vendor/github.com/prometheus/common/expfmt/encode.go index 02b7a5e..8fd8061 100644 --- a/vendor/github.com/prometheus/common/expfmt/encode.go +++ b/vendor/github.com/prometheus/common/expfmt/encode.go @@ -22,6 +22,7 @@ import ( "google.golang.org/protobuf/encoding/prototext" "github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg" + "github.com/prometheus/common/model" dto "github.com/prometheus/client_model/go" ) @@ -61,23 +62,32 @@ func (ec encoderCloser) Close() error { // as the support is still experimental. To include the option to negotiate // FmtOpenMetrics, use NegotiateOpenMetrics. func Negotiate(h http.Header) Format { + escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String()))) for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { + if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { + switch Format(escapeParam) { + case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: + escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam)) + default: + // If the escaping parameter is unknown, ignore it. + } + } ver := ac.Params["version"] if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol { switch ac.Params["encoding"] { case "delimited": - return FmtProtoDelim + return fmtProtoDelim + escapingScheme case "text": - return FmtProtoText + return fmtProtoText + escapingScheme case "compact-text": - return FmtProtoCompact + return fmtProtoCompact + escapingScheme } } if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { - return FmtText + return fmtText + escapingScheme } } - return FmtText + return fmtText + escapingScheme } // NegotiateIncludingOpenMetrics works like Negotiate but includes @@ -85,29 +95,40 @@ func Negotiate(h http.Header) Format { // temporary and will disappear once FmtOpenMetrics is fully supported and as // such may be negotiated by the normal Negotiate function. func NegotiateIncludingOpenMetrics(h http.Header) Format { + escapingScheme := Format(fmt.Sprintf("; escaping=%s", Format(model.NameEscapingScheme.String()))) for _, ac := range goautoneg.ParseAccept(h.Get(hdrAccept)) { + if escapeParam := ac.Params[model.EscapingKey]; escapeParam != "" { + switch Format(escapeParam) { + case model.AllowUTF8, model.EscapeUnderscores, model.EscapeDots, model.EscapeValues: + escapingScheme = Format(fmt.Sprintf("; escaping=%s", escapeParam)) + default: + // If the escaping parameter is unknown, ignore it. + } + } ver := ac.Params["version"] if ac.Type+"/"+ac.SubType == ProtoType && ac.Params["proto"] == ProtoProtocol { switch ac.Params["encoding"] { case "delimited": - return FmtProtoDelim + return fmtProtoDelim + escapingScheme case "text": - return FmtProtoText + return fmtProtoText + escapingScheme case "compact-text": - return FmtProtoCompact + return fmtProtoCompact + escapingScheme } } if ac.Type == "text" && ac.SubType == "plain" && (ver == TextVersion || ver == "") { - return FmtText + return fmtText + escapingScheme } if ac.Type+"/"+ac.SubType == OpenMetricsType && (ver == OpenMetricsVersion_0_0_1 || ver == OpenMetricsVersion_1_0_0 || ver == "") { - if ver == OpenMetricsVersion_1_0_0 { - return FmtOpenMetrics_1_0_0 + switch ver { + case OpenMetricsVersion_1_0_0: + return fmtOpenMetrics_1_0_0 + escapingScheme + default: + return fmtOpenMetrics_0_0_1 + escapingScheme } - return FmtOpenMetrics_0_0_1 } } - return FmtText + return fmtText + escapingScheme } // NewEncoder returns a new encoder based on content type negotiation. All @@ -116,9 +137,13 @@ func NegotiateIncludingOpenMetrics(h http.Header) Format { // for FmtOpenMetrics, but a future (breaking) release will add the Close method // to the Encoder interface directly. The current version of the Encoder // interface is kept for backwards compatibility. +// In cases where the Format does not allow for UTF-8 names, the global +// NameEscapingScheme will be applied. func NewEncoder(w io.Writer, format Format) Encoder { - switch format { - case FmtProtoDelim: + escapingScheme := format.ToEscapingScheme() + + switch format.FormatType() { + case TypeProtoDelim: return encoderCloser{ encode: func(v *dto.MetricFamily) error { _, err := protodelim.MarshalTo(w, v) @@ -126,34 +151,34 @@ func NewEncoder(w io.Writer, format Format) Encoder { }, close: func() error { return nil }, } - case FmtProtoCompact: + case TypeProtoCompact: return encoderCloser{ encode: func(v *dto.MetricFamily) error { - _, err := fmt.Fprintln(w, v.String()) + _, err := fmt.Fprintln(w, model.EscapeMetricFamily(v, escapingScheme).String()) return err }, close: func() error { return nil }, } - case FmtProtoText: + case TypeProtoText: return encoderCloser{ encode: func(v *dto.MetricFamily) error { - _, err := fmt.Fprintln(w, prototext.Format(v)) + _, err := fmt.Fprintln(w, prototext.Format(model.EscapeMetricFamily(v, escapingScheme))) return err }, close: func() error { return nil }, } - case FmtText: + case TypeTextPlain: return encoderCloser{ encode: func(v *dto.MetricFamily) error { - _, err := MetricFamilyToText(w, v) + _, err := MetricFamilyToText(w, model.EscapeMetricFamily(v, escapingScheme)) return err }, close: func() error { return nil }, } - case FmtOpenMetrics_0_0_1, FmtOpenMetrics_1_0_0: + case TypeOpenMetrics: return encoderCloser{ encode: func(v *dto.MetricFamily) error { - _, err := MetricFamilyToOpenMetrics(w, v) + _, err := MetricFamilyToOpenMetrics(w, model.EscapeMetricFamily(v, escapingScheme)) return err }, close: func() error { diff --git a/vendor/github.com/prometheus/common/expfmt/expfmt.go b/vendor/github.com/prometheus/common/expfmt/expfmt.go index c4cb20f..6fc9555 100644 --- a/vendor/github.com/prometheus/common/expfmt/expfmt.go +++ b/vendor/github.com/prometheus/common/expfmt/expfmt.go @@ -14,30 +14,154 @@ // Package expfmt contains tools for reading and writing Prometheus metrics. package expfmt +import ( + "strings" + + "github.com/prometheus/common/model" +) + // Format specifies the HTTP content type of the different wire protocols. type Format string -// Constants to assemble the Content-Type values for the different wire protocols. +// Constants to assemble the Content-Type values for the different wire +// protocols. The Content-Type strings here are all for the legacy exposition +// formats, where valid characters for metric names and label names are limited. +// Support for arbitrary UTF-8 characters in those names is already partially +// implemented in this module (see model.ValidationScheme), but to actually use +// it on the wire, new content-type strings will have to be agreed upon and +// added here. const ( TextVersion = "0.0.4" ProtoType = `application/vnd.google.protobuf` ProtoProtocol = `io.prometheus.client.MetricFamily` - ProtoFmt = ProtoType + "; proto=" + ProtoProtocol + ";" + protoFmt = ProtoType + "; proto=" + ProtoProtocol + ";" OpenMetricsType = `application/openmetrics-text` OpenMetricsVersion_0_0_1 = "0.0.1" OpenMetricsVersion_1_0_0 = "1.0.0" - // The Content-Type values for the different wire protocols. - FmtUnknown Format = `` - FmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8` - FmtProtoDelim Format = ProtoFmt + ` encoding=delimited` - FmtProtoText Format = ProtoFmt + ` encoding=text` - FmtProtoCompact Format = ProtoFmt + ` encoding=compact-text` - FmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8` - FmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8` + // The Content-Type values for the different wire protocols. Note that these + // values are now unexported. If code was relying on comparisons to these + // constants, instead use FormatType(). + fmtUnknown Format = `` + fmtText Format = `text/plain; version=` + TextVersion + `; charset=utf-8` + fmtProtoDelim Format = protoFmt + ` encoding=delimited` + fmtProtoText Format = protoFmt + ` encoding=text` + fmtProtoCompact Format = protoFmt + ` encoding=compact-text` + fmtOpenMetrics_1_0_0 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_1_0_0 + `; charset=utf-8` + fmtOpenMetrics_0_0_1 Format = OpenMetricsType + `; version=` + OpenMetricsVersion_0_0_1 + `; charset=utf-8` ) const ( hdrContentType = "Content-Type" hdrAccept = "Accept" ) + +// FormatType is a Go enum representing the overall category for the given +// Format. As the number of Format permutations increases, doing basic string +// comparisons are not feasible, so this enum captures the most useful +// high-level attribute of the Format string. +type FormatType int + +const ( + TypeUnknown = iota + TypeProtoCompact + TypeProtoDelim + TypeProtoText + TypeTextPlain + TypeOpenMetrics +) + +// NewFormat generates a new Format from the type provided. Mostly used for +// tests, most Formats should be generated as part of content negotiation in +// encode.go. +func NewFormat(t FormatType) Format { + switch t { + case TypeProtoCompact: + return fmtProtoCompact + case TypeProtoDelim: + return fmtProtoDelim + case TypeProtoText: + return fmtProtoText + case TypeTextPlain: + return fmtText + case TypeOpenMetrics: + return fmtOpenMetrics_1_0_0 + default: + return fmtUnknown + } +} + +// FormatType deduces an overall FormatType for the given format. +func (f Format) FormatType() FormatType { + toks := strings.Split(string(f), ";") + if len(toks) < 2 { + return TypeUnknown + } + + params := make(map[string]string) + for i, t := range toks { + if i == 0 { + continue + } + args := strings.Split(t, "=") + if len(args) != 2 { + continue + } + params[strings.TrimSpace(args[0])] = strings.TrimSpace(args[1]) + } + + switch strings.TrimSpace(toks[0]) { + case ProtoType: + if params["proto"] != ProtoProtocol { + return TypeUnknown + } + switch params["encoding"] { + case "delimited": + return TypeProtoDelim + case "text": + return TypeProtoText + case "compact-text": + return TypeProtoCompact + default: + return TypeUnknown + } + case OpenMetricsType: + if params["charset"] != "utf-8" { + return TypeUnknown + } + return TypeOpenMetrics + case "text/plain": + v, ok := params["version"] + if !ok { + return TypeTextPlain + } + if v == TextVersion { + return TypeTextPlain + } + return TypeUnknown + default: + return TypeUnknown + } +} + +// ToEscapingScheme returns an EscapingScheme depending on the Format. Iff the +// Format contains a escaping=allow-utf-8 term, it will select NoEscaping. If a valid +// "escaping" term exists, that will be used. Otherwise, the global default will +// be returned. +func (format Format) ToEscapingScheme() model.EscapingScheme { + for _, p := range strings.Split(string(format), ";") { + toks := strings.Split(p, "=") + if len(toks) != 2 { + continue + } + key, value := strings.TrimSpace(toks[0]), strings.TrimSpace(toks[1]) + if key == model.EscapingKey { + scheme, err := model.ToEscapingScheme(value) + if err != nil { + return model.NameEscapingScheme + } + return scheme + } + } + return model.NameEscapingScheme +} diff --git a/vendor/github.com/prometheus/common/expfmt/openmetrics_create.go b/vendor/github.com/prometheus/common/expfmt/openmetrics_create.go index 21cdddc..5622578 100644 --- a/vendor/github.com/prometheus/common/expfmt/openmetrics_create.go +++ b/vendor/github.com/prometheus/common/expfmt/openmetrics_create.go @@ -35,6 +35,18 @@ import ( // sanity checks. If the input contains duplicate metrics or invalid metric or // label names, the conversion will result in invalid text format output. // +// If metric names conform to the legacy validation pattern, they will be placed +// outside the brackets in the traditional way, like `foo{}`. If the metric name +// fails the legacy validation check, it will be placed quoted inside the +// brackets: `{"foo"}`. As stated above, the input is assumed to be santized and +// no error will be thrown in this case. +// +// Similar to metric names, if label names conform to the legacy validation +// pattern, they will be unquoted as normal, like `foo{bar="baz"}`. If the label +// name fails the legacy validation check, it will be quoted: +// `foo{"bar"="baz"}`. As stated above, the input is assumed to be santized and +// no error will be thrown in this case. +// // This function fulfills the type 'expfmt.encoder'. // // Note that OpenMetrics requires a final `# EOF` line. Since this function acts @@ -98,7 +110,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int if err != nil { return } - n, err = w.WriteString(shortName) + n, err = writeName(w, shortName) written += n if err != nil { return @@ -124,7 +136,7 @@ func MetricFamilyToOpenMetrics(out io.Writer, in *dto.MetricFamily) (written int if err != nil { return } - n, err = w.WriteString(shortName) + n, err = writeName(w, shortName) written += n if err != nil { return @@ -303,21 +315,9 @@ func writeOpenMetricsSample( floatValue float64, intValue uint64, useIntValue bool, exemplar *dto.Exemplar, ) (int, error) { - var written int - n, err := w.WriteString(name) - written += n - if err != nil { - return written, err - } - if suffix != "" { - n, err = w.WriteString(suffix) - written += n - if err != nil { - return written, err - } - } - n, err = writeOpenMetricsLabelPairs( - w, metric.Label, additionalLabelName, additionalLabelValue, + written := 0 + n, err := writeOpenMetricsNameAndLabelPairs( + w, name+suffix, metric.Label, additionalLabelName, additionalLabelValue, ) written += n if err != nil { @@ -365,27 +365,58 @@ func writeOpenMetricsSample( return written, nil } -// writeOpenMetricsLabelPairs works like writeOpenMetrics but formats the float -// in OpenMetrics style. -func writeOpenMetricsLabelPairs( +// writeOpenMetricsNameAndLabelPairs works like writeOpenMetricsSample but +// formats the float in OpenMetrics style. +func writeOpenMetricsNameAndLabelPairs( w enhancedWriter, + name string, in []*dto.LabelPair, additionalLabelName string, additionalLabelValue float64, ) (int, error) { - if len(in) == 0 && additionalLabelName == "" { - return 0, nil - } var ( - written int - separator byte = '{' + written int + separator byte = '{' + metricInsideBraces = false ) + + if name != "" { + // If the name does not pass the legacy validity check, we must put the + // metric name inside the braces, quoted. + if !model.IsValidLegacyMetricName(model.LabelValue(name)) { + metricInsideBraces = true + err := w.WriteByte(separator) + written++ + if err != nil { + return written, err + } + separator = ',' + } + + n, err := writeName(w, name) + written += n + if err != nil { + return written, err + } + } + + if len(in) == 0 && additionalLabelName == "" { + if metricInsideBraces { + err := w.WriteByte('}') + written++ + if err != nil { + return written, err + } + } + return written, nil + } + for _, lp := range in { err := w.WriteByte(separator) written++ if err != nil { return written, err } - n, err := w.WriteString(lp.GetName()) + n, err := writeName(w, lp.GetName()) written += n if err != nil { return written, err @@ -451,7 +482,7 @@ func writeExemplar(w enhancedWriter, e *dto.Exemplar) (int, error) { if err != nil { return written, err } - n, err = writeOpenMetricsLabelPairs(w, e.Label, "", 0) + n, err = writeOpenMetricsNameAndLabelPairs(w, "", e.Label, "", 0) written += n if err != nil { return written, err diff --git a/vendor/github.com/prometheus/common/expfmt/text_create.go b/vendor/github.com/prometheus/common/expfmt/text_create.go index 2946b8f..f9b8265 100644 --- a/vendor/github.com/prometheus/common/expfmt/text_create.go +++ b/vendor/github.com/prometheus/common/expfmt/text_create.go @@ -62,6 +62,18 @@ var ( // contains duplicate metrics or invalid metric or label names, the conversion // will result in invalid text format output. // +// If metric names conform to the legacy validation pattern, they will be placed +// outside the brackets in the traditional way, like `foo{}`. If the metric name +// fails the legacy validation check, it will be placed quoted inside the +// brackets: `{"foo"}`. As stated above, the input is assumed to be santized and +// no error will be thrown in this case. +// +// Similar to metric names, if label names conform to the legacy validation +// pattern, they will be unquoted as normal, like `foo{bar="baz"}`. If the label +// name fails the legacy validation check, it will be quoted: +// `foo{"bar"="baz"}`. As stated above, the input is assumed to be santized and +// no error will be thrown in this case. +// // This method fulfills the type 'prometheus.encoder'. func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err error) { // Fail-fast checks. @@ -98,7 +110,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e if err != nil { return } - n, err = w.WriteString(name) + n, err = writeName(w, name) written += n if err != nil { return @@ -124,7 +136,7 @@ func MetricFamilyToText(out io.Writer, in *dto.MetricFamily) (written int, err e if err != nil { return } - n, err = w.WriteString(name) + n, err = writeName(w, name) written += n if err != nil { return @@ -280,21 +292,9 @@ func writeSample( additionalLabelName string, additionalLabelValue float64, value float64, ) (int, error) { - var written int - n, err := w.WriteString(name) - written += n - if err != nil { - return written, err - } - if suffix != "" { - n, err = w.WriteString(suffix) - written += n - if err != nil { - return written, err - } - } - n, err = writeLabelPairs( - w, metric.Label, additionalLabelName, additionalLabelValue, + written := 0 + n, err := writeNameAndLabelPairs( + w, name+suffix, metric.Label, additionalLabelName, additionalLabelValue, ) written += n if err != nil { @@ -330,32 +330,64 @@ func writeSample( return written, nil } -// writeLabelPairs converts a slice of LabelPair proto messages plus the -// explicitly given additional label pair into text formatted as required by the -// text format and writes it to 'w'. An empty slice in combination with an empty -// string 'additionalLabelName' results in nothing being written. Otherwise, the -// label pairs are written, escaped as required by the text format, and enclosed -// in '{...}'. The function returns the number of bytes written and any error -// encountered. -func writeLabelPairs( +// writeNameAndLabelPairs converts a slice of LabelPair proto messages plus the +// explicitly given metric name and additional label pair into text formatted as +// required by the text format and writes it to 'w'. An empty slice in +// combination with an empty string 'additionalLabelName' results in nothing +// being written. Otherwise, the label pairs are written, escaped as required by +// the text format, and enclosed in '{...}'. The function returns the number of +// bytes written and any error encountered. If the metric name is not +// legacy-valid, it will be put inside the brackets as well. Legacy-invalid +// label names will also be quoted. +func writeNameAndLabelPairs( w enhancedWriter, + name string, in []*dto.LabelPair, additionalLabelName string, additionalLabelValue float64, ) (int, error) { - if len(in) == 0 && additionalLabelName == "" { - return 0, nil - } var ( - written int - separator byte = '{' + written int + separator byte = '{' + metricInsideBraces = false ) + + if name != "" { + // If the name does not pass the legacy validity check, we must put the + // metric name inside the braces. + if !model.IsValidLegacyMetricName(model.LabelValue(name)) { + metricInsideBraces = true + err := w.WriteByte(separator) + written++ + if err != nil { + return written, err + } + separator = ',' + } + n, err := writeName(w, name) + written += n + if err != nil { + return written, err + } + } + + if len(in) == 0 && additionalLabelName == "" { + if metricInsideBraces { + err := w.WriteByte('}') + written++ + if err != nil { + return written, err + } + } + return written, nil + } + for _, lp := range in { err := w.WriteByte(separator) written++ if err != nil { return written, err } - n, err := w.WriteString(lp.GetName()) + n, err := writeName(w, lp.GetName()) written += n if err != nil { return written, err @@ -462,3 +494,27 @@ func writeInt(w enhancedWriter, i int64) (int, error) { numBufPool.Put(bp) return written, err } + +// writeName writes a string as-is if it complies with the legacy naming +// scheme, or escapes it in double quotes if not. +func writeName(w enhancedWriter, name string) (int, error) { + if model.IsValidLegacyMetricName(model.LabelValue(name)) { + return w.WriteString(name) + } + var written int + var err error + err = w.WriteByte('"') + written++ + if err != nil { + return written, err + } + var n int + n, err = writeEscapedString(w, name, true) + written += n + if err != nil { + return written, err + } + err = w.WriteByte('"') + written++ + return written, err +} diff --git a/vendor/github.com/prometheus/common/model/labels.go b/vendor/github.com/prometheus/common/model/labels.go index ef89563..3317ce2 100644 --- a/vendor/github.com/prometheus/common/model/labels.go +++ b/vendor/github.com/prometheus/common/model/labels.go @@ -97,17 +97,25 @@ var LabelNameRE = regexp.MustCompile("^[a-zA-Z_][a-zA-Z0-9_]*$") // therewith. type LabelName string -// IsValid is true iff the label name matches the pattern of LabelNameRE. This -// method, however, does not use LabelNameRE for the check but a much faster -// hardcoded implementation. +// IsValid returns true iff name matches the pattern of LabelNameRE for legacy +// names, and iff it's valid UTF-8 if NameValidationScheme is set to +// UTF8Validation. For the legacy matching, it does not use LabelNameRE for the +// check but a much faster hardcoded implementation. func (ln LabelName) IsValid() bool { if len(ln) == 0 { return false } - for i, b := range ln { - if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { - return false + switch NameValidationScheme { + case LegacyValidation: + for i, b := range ln { + if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || (b >= '0' && b <= '9' && i > 0)) { + return false + } } + case UTF8Validation: + return utf8.ValidString(string(ln)) + default: + panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme)) } return true } @@ -164,7 +172,7 @@ func (l LabelNames) String() string { // A LabelValue is an associated value for a LabelName. type LabelValue string -// IsValid returns true iff the string is a valid UTF8. +// IsValid returns true iff the string is a valid UTF-8. func (lv LabelValue) IsValid() bool { return utf8.ValidString(string(lv)) } diff --git a/vendor/github.com/prometheus/common/model/metric.go b/vendor/github.com/prometheus/common/model/metric.go index f8c5eab..0bd29b3 100644 --- a/vendor/github.com/prometheus/common/model/metric.go +++ b/vendor/github.com/prometheus/common/model/metric.go @@ -18,6 +18,77 @@ import ( "regexp" "sort" "strings" + "unicode/utf8" + + dto "github.com/prometheus/client_model/go" + "google.golang.org/protobuf/proto" +) + +var ( + // NameValidationScheme determines the method of name validation to be used by + // all calls to IsValidMetricName() and LabelName IsValid(). Setting UTF-8 mode + // in isolation from other components that don't support UTF-8 may result in + // bugs or other undefined behavior. This value is intended to be set by + // UTF-8-aware binaries as part of their startup. To avoid need for locking, + // this value should be set once, ideally in an init(), before multiple + // goroutines are started. + NameValidationScheme = LegacyValidation + + // NameEscapingScheme defines the default way that names will be + // escaped when presented to systems that do not support UTF-8 names. If the + // Content-Type "escaping" term is specified, that will override this value. + NameEscapingScheme = ValueEncodingEscaping +) + +// ValidationScheme is a Go enum for determining how metric and label names will +// be validated by this library. +type ValidationScheme int + +const ( + // LegacyValidation is a setting that requirets that metric and label names + // conform to the original Prometheus character requirements described by + // MetricNameRE and LabelNameRE. + LegacyValidation ValidationScheme = iota + + // UTF8Validation only requires that metric and label names be valid UTF-8 + // strings. + UTF8Validation +) + +type EscapingScheme int + +const ( + // NoEscaping indicates that a name will not be escaped. Unescaped names that + // do not conform to the legacy validity check will use a new exposition + // format syntax that will be officially standardized in future versions. + NoEscaping EscapingScheme = iota + + // UnderscoreEscaping replaces all legacy-invalid characters with underscores. + UnderscoreEscaping + + // DotsEscaping is similar to UnderscoreEscaping, except that dots are + // converted to `_dot_` and pre-existing underscores are converted to `__`. + DotsEscaping + + // ValueEncodingEscaping prepends the name with `U__` and replaces all invalid + // characters with the unicode value, surrounded by underscores. Single + // underscores are replaced with double underscores. + ValueEncodingEscaping +) + +const ( + // EscapingKey is the key in an Accept or Content-Type header that defines how + // metric and label names that do not conform to the legacy character + // requirements should be escaped when being scraped by a legacy prometheus + // system. If a system does not explicitly pass an escaping parameter in the + // Accept header, the default NameEscapingScheme will be used. + EscapingKey = "escaping" + + // Possible values for Escaping Key: + AllowUTF8 = "allow-utf-8" // No escaping required. + EscapeUnderscores = "underscores" + EscapeDots = "dots" + EscapeValues = "values" ) // MetricNameRE is a regular expression matching valid metric @@ -84,17 +155,302 @@ func (m Metric) FastFingerprint() Fingerprint { return LabelSet(m).FastFingerprint() } -// IsValidMetricName returns true iff name matches the pattern of MetricNameRE. +// IsValidMetricName returns true iff name matches the pattern of MetricNameRE +// for legacy names, and iff it's valid UTF-8 if the UTF8Validation scheme is +// selected. +func IsValidMetricName(n LabelValue) bool { + switch NameValidationScheme { + case LegacyValidation: + return IsValidLegacyMetricName(n) + case UTF8Validation: + if len(n) == 0 { + return false + } + return utf8.ValidString(string(n)) + default: + panic(fmt.Sprintf("Invalid name validation scheme requested: %d", NameValidationScheme)) + } +} + +// IsValidLegacyMetricName is similar to IsValidMetricName but always uses the +// legacy validation scheme regardless of the value of NameValidationScheme. // This function, however, does not use MetricNameRE for the check but a much // faster hardcoded implementation. -func IsValidMetricName(n LabelValue) bool { +func IsValidLegacyMetricName(n LabelValue) bool { if len(n) == 0 { return false } for i, b := range n { - if !((b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0)) { + if !isValidLegacyRune(b, i) { return false } } return true } + +// EscapeMetricFamily escapes the given metric names and labels with the given +// escaping scheme. Returns a new object that uses the same pointers to fields +// when possible and creates new escaped versions so as not to mutate the +// input. +func EscapeMetricFamily(v *dto.MetricFamily, scheme EscapingScheme) *dto.MetricFamily { + if v == nil { + return nil + } + + if scheme == NoEscaping { + return v + } + + out := &dto.MetricFamily{ + Help: v.Help, + Type: v.Type, + } + + // If the name is nil, copy as-is, don't try to escape. + if v.Name == nil || IsValidLegacyMetricName(LabelValue(v.GetName())) { + out.Name = v.Name + } else { + out.Name = proto.String(EscapeName(v.GetName(), scheme)) + } + for _, m := range v.Metric { + if !metricNeedsEscaping(m) { + out.Metric = append(out.Metric, m) + continue + } + + escaped := &dto.Metric{ + Gauge: m.Gauge, + Counter: m.Counter, + Summary: m.Summary, + Untyped: m.Untyped, + Histogram: m.Histogram, + TimestampMs: m.TimestampMs, + } + + for _, l := range m.Label { + if l.GetName() == MetricNameLabel { + if l.Value == nil || IsValidLegacyMetricName(LabelValue(l.GetValue())) { + escaped.Label = append(escaped.Label, l) + continue + } + escaped.Label = append(escaped.Label, &dto.LabelPair{ + Name: proto.String(MetricNameLabel), + Value: proto.String(EscapeName(l.GetValue(), scheme)), + }) + continue + } + if l.Name == nil || IsValidLegacyMetricName(LabelValue(l.GetName())) { + escaped.Label = append(escaped.Label, l) + continue + } + escaped.Label = append(escaped.Label, &dto.LabelPair{ + Name: proto.String(EscapeName(l.GetName(), scheme)), + Value: l.Value, + }) + } + out.Metric = append(out.Metric, escaped) + } + return out +} + +func metricNeedsEscaping(m *dto.Metric) bool { + for _, l := range m.Label { + if l.GetName() == MetricNameLabel && !IsValidLegacyMetricName(LabelValue(l.GetValue())) { + return true + } + if !IsValidLegacyMetricName(LabelValue(l.GetName())) { + return true + } + } + return false +} + +const ( + lowerhex = "0123456789abcdef" +) + +// EscapeName escapes the incoming name according to the provided escaping +// scheme. Depending on the rules of escaping, this may cause no change in the +// string that is returned. (Especially NoEscaping, which by definition is a +// noop). This function does not do any validation of the name. +func EscapeName(name string, scheme EscapingScheme) string { + if len(name) == 0 { + return name + } + var escaped strings.Builder + switch scheme { + case NoEscaping: + return name + case UnderscoreEscaping: + if IsValidLegacyMetricName(LabelValue(name)) { + return name + } + for i, b := range name { + if isValidLegacyRune(b, i) { + escaped.WriteRune(b) + } else { + escaped.WriteRune('_') + } + } + return escaped.String() + case DotsEscaping: + // Do not early return for legacy valid names, we still escape underscores. + for i, b := range name { + if b == '_' { + escaped.WriteString("__") + } else if b == '.' { + escaped.WriteString("_dot_") + } else if isValidLegacyRune(b, i) { + escaped.WriteRune(b) + } else { + escaped.WriteRune('_') + } + } + return escaped.String() + case ValueEncodingEscaping: + if IsValidLegacyMetricName(LabelValue(name)) { + return name + } + escaped.WriteString("U__") + for i, b := range name { + if isValidLegacyRune(b, i) { + escaped.WriteRune(b) + } else if !utf8.ValidRune(b) { + escaped.WriteString("_FFFD_") + } else if b < 0x100 { + escaped.WriteRune('_') + for s := 4; s >= 0; s -= 4 { + escaped.WriteByte(lowerhex[b>>uint(s)&0xF]) + } + escaped.WriteRune('_') + } else if b < 0x10000 { + escaped.WriteRune('_') + for s := 12; s >= 0; s -= 4 { + escaped.WriteByte(lowerhex[b>>uint(s)&0xF]) + } + escaped.WriteRune('_') + } + } + return escaped.String() + default: + panic(fmt.Sprintf("invalid escaping scheme %d", scheme)) + } +} + +// lower function taken from strconv.atoi +func lower(c byte) byte { + return c | ('x' - 'X') +} + +// UnescapeName unescapes the incoming name according to the provided escaping +// scheme if possible. Some schemes are partially or totally non-roundtripable. +// If any error is enountered, returns the original input. +func UnescapeName(name string, scheme EscapingScheme) string { + if len(name) == 0 { + return name + } + switch scheme { + case NoEscaping: + return name + case UnderscoreEscaping: + // It is not possible to unescape from underscore replacement. + return name + case DotsEscaping: + name = strings.ReplaceAll(name, "_dot_", ".") + name = strings.ReplaceAll(name, "__", "_") + return name + case ValueEncodingEscaping: + escapedName, found := strings.CutPrefix(name, "U__") + if !found { + return name + } + + var unescaped strings.Builder + TOP: + for i := 0; i < len(escapedName); i++ { + // All non-underscores are treated normally. + if escapedName[i] != '_' { + unescaped.WriteByte(escapedName[i]) + continue + } + i++ + if i >= len(escapedName) { + return name + } + // A double underscore is a single underscore. + if escapedName[i] == '_' { + unescaped.WriteByte('_') + continue + } + // We think we are in a UTF-8 code, process it. + var utf8Val uint + for j := 0; i < len(escapedName); j++ { + // This is too many characters for a utf8 value. + if j > 4 { + return name + } + // Found a closing underscore, convert to a rune, check validity, and append. + if escapedName[i] == '_' { + utf8Rune := rune(utf8Val) + if !utf8.ValidRune(utf8Rune) { + return name + } + unescaped.WriteRune(utf8Rune) + continue TOP + } + r := lower(escapedName[i]) + utf8Val *= 16 + if r >= '0' && r <= '9' { + utf8Val += uint(r) - '0' + } else if r >= 'a' && r <= 'f' { + utf8Val += uint(r) - 'a' + 10 + } else { + return name + } + i++ + } + // Didn't find closing underscore, invalid. + return name + } + return unescaped.String() + default: + panic(fmt.Sprintf("invalid escaping scheme %d", scheme)) + } +} + +func isValidLegacyRune(b rune, i int) bool { + return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z') || b == '_' || b == ':' || (b >= '0' && b <= '9' && i > 0) +} + +func (e EscapingScheme) String() string { + switch e { + case NoEscaping: + return AllowUTF8 + case UnderscoreEscaping: + return EscapeUnderscores + case DotsEscaping: + return EscapeDots + case ValueEncodingEscaping: + return EscapeValues + default: + panic(fmt.Sprintf("unknown format scheme %d", e)) + } +} + +func ToEscapingScheme(s string) (EscapingScheme, error) { + if s == "" { + return NoEscaping, fmt.Errorf("got empty string instead of escaping scheme") + } + switch s { + case AllowUTF8: + return NoEscaping, nil + case EscapeUnderscores: + return UnderscoreEscaping, nil + case EscapeDots: + return DotsEscaping, nil + case EscapeValues: + return ValueEncodingEscaping, nil + default: + return NoEscaping, fmt.Errorf("unknown format scheme " + s) + } +} diff --git a/vendor/golang.org/x/exp/constraints/constraints.go b/vendor/golang.org/x/exp/constraints/constraints.go new file mode 100644 index 0000000..2c033df --- /dev/null +++ b/vendor/golang.org/x/exp/constraints/constraints.go @@ -0,0 +1,50 @@ +// Copyright 2021 The Go 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 constraints defines a set of useful constraints to be used +// with type parameters. +package constraints + +// Signed is a constraint that permits any signed integer type. +// If future releases of Go add new predeclared signed integer types, +// this constraint will be modified to include them. +type Signed interface { + ~int | ~int8 | ~int16 | ~int32 | ~int64 +} + +// Unsigned is a constraint that permits any unsigned integer type. +// If future releases of Go add new predeclared unsigned integer types, +// this constraint will be modified to include them. +type Unsigned interface { + ~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr +} + +// Integer is a constraint that permits any integer type. +// If future releases of Go add new predeclared integer types, +// this constraint will be modified to include them. +type Integer interface { + Signed | Unsigned +} + +// Float is a constraint that permits any floating-point type. +// If future releases of Go add new predeclared floating-point types, +// this constraint will be modified to include them. +type Float interface { + ~float32 | ~float64 +} + +// Complex is a constraint that permits any complex numeric type. +// If future releases of Go add new predeclared complex numeric types, +// this constraint will be modified to include them. +type Complex interface { + ~complex64 | ~complex128 +} + +// Ordered is a constraint that permits any ordered type: any type +// that supports the operators < <= >= >. +// If future releases of Go add new ordered types, +// this constraint will be modified to include them. +type Ordered interface { + Integer | Float | ~string +} diff --git a/vendor/golang.org/x/sys/unix/mkerrors.sh b/vendor/golang.org/x/sys/unix/mkerrors.sh index c649202..fdcaa97 100644 --- a/vendor/golang.org/x/sys/unix/mkerrors.sh +++ b/vendor/golang.org/x/sys/unix/mkerrors.sh @@ -584,7 +584,7 @@ ccflags="$@" $2 ~ /^KEY_(SPEC|REQKEY_DEFL)_/ || $2 ~ /^KEYCTL_/ || $2 ~ /^PERF_/ || - $2 ~ /^SECCOMP_MODE_/ || + $2 ~ /^SECCOMP_/ || $2 ~ /^SEEK_/ || $2 ~ /^SCHED_/ || $2 ~ /^SPLICE_/ || diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux.go b/vendor/golang.org/x/sys/unix/zerrors_linux.go index a5d3ff8..36bf839 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux.go @@ -1785,6 +1785,8 @@ const ( LANDLOCK_ACCESS_FS_REMOVE_FILE = 0x20 LANDLOCK_ACCESS_FS_TRUNCATE = 0x4000 LANDLOCK_ACCESS_FS_WRITE_FILE = 0x2 + LANDLOCK_ACCESS_NET_BIND_TCP = 0x1 + LANDLOCK_ACCESS_NET_CONNECT_TCP = 0x2 LANDLOCK_CREATE_RULESET_VERSION = 0x1 LINUX_REBOOT_CMD_CAD_OFF = 0x0 LINUX_REBOOT_CMD_CAD_ON = 0x89abcdef @@ -2465,6 +2467,7 @@ const ( PR_MCE_KILL_GET = 0x22 PR_MCE_KILL_LATE = 0x0 PR_MCE_KILL_SET = 0x1 + PR_MDWE_NO_INHERIT = 0x2 PR_MDWE_REFUSE_EXEC_GAIN = 0x1 PR_MPX_DISABLE_MANAGEMENT = 0x2c PR_MPX_ENABLE_MANAGEMENT = 0x2b @@ -2669,8 +2672,9 @@ const ( RTAX_FEATURES = 0xc RTAX_FEATURE_ALLFRAG = 0x8 RTAX_FEATURE_ECN = 0x1 - RTAX_FEATURE_MASK = 0xf + RTAX_FEATURE_MASK = 0x1f RTAX_FEATURE_SACK = 0x2 + RTAX_FEATURE_TCP_USEC_TS = 0x10 RTAX_FEATURE_TIMESTAMP = 0x4 RTAX_HOPLIMIT = 0xa RTAX_INITCWND = 0xb @@ -2913,9 +2917,38 @@ const ( SCM_RIGHTS = 0x1 SCM_TIMESTAMP = 0x1d SC_LOG_FLUSH = 0x100000 + SECCOMP_ADDFD_FLAG_SEND = 0x2 + SECCOMP_ADDFD_FLAG_SETFD = 0x1 + SECCOMP_FILTER_FLAG_LOG = 0x2 + SECCOMP_FILTER_FLAG_NEW_LISTENER = 0x8 + SECCOMP_FILTER_FLAG_SPEC_ALLOW = 0x4 + SECCOMP_FILTER_FLAG_TSYNC = 0x1 + SECCOMP_FILTER_FLAG_TSYNC_ESRCH = 0x10 + SECCOMP_FILTER_FLAG_WAIT_KILLABLE_RECV = 0x20 + SECCOMP_GET_ACTION_AVAIL = 0x2 + SECCOMP_GET_NOTIF_SIZES = 0x3 + SECCOMP_IOCTL_NOTIF_RECV = 0xc0502100 + SECCOMP_IOCTL_NOTIF_SEND = 0xc0182101 + SECCOMP_IOC_MAGIC = '!' SECCOMP_MODE_DISABLED = 0x0 SECCOMP_MODE_FILTER = 0x2 SECCOMP_MODE_STRICT = 0x1 + SECCOMP_RET_ACTION = 0x7fff0000 + SECCOMP_RET_ACTION_FULL = 0xffff0000 + SECCOMP_RET_ALLOW = 0x7fff0000 + SECCOMP_RET_DATA = 0xffff + SECCOMP_RET_ERRNO = 0x50000 + SECCOMP_RET_KILL = 0x0 + SECCOMP_RET_KILL_PROCESS = 0x80000000 + SECCOMP_RET_KILL_THREAD = 0x0 + SECCOMP_RET_LOG = 0x7ffc0000 + SECCOMP_RET_TRACE = 0x7ff00000 + SECCOMP_RET_TRAP = 0x30000 + SECCOMP_RET_USER_NOTIF = 0x7fc00000 + SECCOMP_SET_MODE_FILTER = 0x1 + SECCOMP_SET_MODE_STRICT = 0x0 + SECCOMP_USER_NOTIF_FD_SYNC_WAKE_UP = 0x1 + SECCOMP_USER_NOTIF_FLAG_CONTINUE = 0x1 SECRETMEM_MAGIC = 0x5345434d SECURITYFS_MAGIC = 0x73636673 SEEK_CUR = 0x1 @@ -3075,6 +3108,7 @@ const ( SOL_TIPC = 0x10f SOL_TLS = 0x11a SOL_UDP = 0x11 + SOL_VSOCK = 0x11f SOL_X25 = 0x106 SOL_XDP = 0x11b SOMAXCONN = 0x1000 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go index 4920821..42ff8c3 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_386.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go index a0c1e41..dca4360 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_amd64.go @@ -282,6 +282,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go index c639855..5cca668 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm.go @@ -288,6 +288,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go index 47cc62e..d8cae6d 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_arm64.go @@ -278,6 +278,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go index 27ac4a0..28e39af 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_loong64.go @@ -275,6 +275,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go index 5469464..cd66e92 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go index 3adb81d..c1595eb 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go index 2dfe98f..ee9456b 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mips64le.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go index f5398f8..8cfca81 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_mipsle.go @@ -281,6 +281,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x80 SIOCATMARK = 0x40047307 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go index c54f152..60b0deb 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc.go @@ -336,6 +336,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go index 76057dc..f90aa72 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64.go @@ -340,6 +340,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go index e0c3725..ba9e015 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_ppc64le.go @@ -340,6 +340,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go index 18f2813..07cdfd6 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_riscv64.go @@ -272,6 +272,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go index 11619d4..2f1dd21 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_s390x.go @@ -344,6 +344,9 @@ const ( SCM_TIMESTAMPNS = 0x23 SCM_TXTIME = 0x3d SCM_WIFI_STATUS = 0x29 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x40182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x40082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x40082104 SFD_CLOEXEC = 0x80000 SFD_NONBLOCK = 0x800 SIOCATMARK = 0x8905 diff --git a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go index 396d994..f40519d 100644 --- a/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zerrors_linux_sparc64.go @@ -335,6 +335,9 @@ const ( SCM_TIMESTAMPNS = 0x21 SCM_TXTIME = 0x3f SCM_WIFI_STATUS = 0x25 + SECCOMP_IOCTL_NOTIF_ADDFD = 0x80182103 + SECCOMP_IOCTL_NOTIF_ID_VALID = 0x80082102 + SECCOMP_IOCTL_NOTIF_SET_FLAGS = 0x80082104 SFD_CLOEXEC = 0x400000 SFD_NONBLOCK = 0x4000 SF_FP = 0x38 diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go index fcf3ecb..0cc3ce4 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_386.go @@ -448,4 +448,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go index f56dc25..856d92d 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_amd64.go @@ -371,4 +371,7 @@ const ( SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go index 974bf24..8d46709 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm.go @@ -412,4 +412,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go index 39a2739..edc1732 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_arm64.go @@ -315,4 +315,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go index cf9c9d7..445eba2 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_loong64.go @@ -309,4 +309,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go index 10b7362..adba01b 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips.go @@ -432,4 +432,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 4450 SYS_CACHESTAT = 4451 SYS_FCHMODAT2 = 4452 + SYS_MAP_SHADOW_STACK = 4453 + SYS_FUTEX_WAKE = 4454 + SYS_FUTEX_WAIT = 4455 + SYS_FUTEX_REQUEUE = 4456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go index cd4d8b4..014c4e9 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64.go @@ -362,4 +362,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 5450 SYS_CACHESTAT = 5451 SYS_FCHMODAT2 = 5452 + SYS_MAP_SHADOW_STACK = 5453 + SYS_FUTEX_WAKE = 5454 + SYS_FUTEX_WAIT = 5455 + SYS_FUTEX_REQUEUE = 5456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go index 2c0efca..ccc97d7 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mips64le.go @@ -362,4 +362,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 5450 SYS_CACHESTAT = 5451 SYS_FCHMODAT2 = 5452 + SYS_MAP_SHADOW_STACK = 5453 + SYS_FUTEX_WAKE = 5454 + SYS_FUTEX_WAIT = 5455 + SYS_FUTEX_REQUEUE = 5456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go index a72e31d..ec2b64a 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_mipsle.go @@ -432,4 +432,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 4450 SYS_CACHESTAT = 4451 SYS_FCHMODAT2 = 4452 + SYS_MAP_SHADOW_STACK = 4453 + SYS_FUTEX_WAKE = 4454 + SYS_FUTEX_WAIT = 4455 + SYS_FUTEX_REQUEUE = 4456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go index c7d1e37..21a839e 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc.go @@ -439,4 +439,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go index f4d4838..c11121e 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64.go @@ -411,4 +411,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go index b64f0e5..909b631 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_ppc64le.go @@ -411,4 +411,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go index 9571119..e49bed1 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_riscv64.go @@ -316,4 +316,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go index f94e943..66017d2 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_s390x.go @@ -377,4 +377,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go b/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go index ba0c2bc..47bab18 100644 --- a/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go +++ b/vendor/golang.org/x/sys/unix/zsysnum_linux_sparc64.go @@ -390,4 +390,8 @@ const ( SYS_SET_MEMPOLICY_HOME_NODE = 450 SYS_CACHESTAT = 451 SYS_FCHMODAT2 = 452 + SYS_MAP_SHADOW_STACK = 453 + SYS_FUTEX_WAKE = 454 + SYS_FUTEX_WAIT = 455 + SYS_FUTEX_REQUEUE = 456 ) diff --git a/vendor/golang.org/x/sys/unix/ztypes_linux.go b/vendor/golang.org/x/sys/unix/ztypes_linux.go index bbf8399..dc0c955 100644 --- a/vendor/golang.org/x/sys/unix/ztypes_linux.go +++ b/vendor/golang.org/x/sys/unix/ztypes_linux.go @@ -174,7 +174,8 @@ type FscryptPolicyV2 struct { Contents_encryption_mode uint8 Filenames_encryption_mode uint8 Flags uint8 - _ [4]uint8 + Log2_data_unit_size uint8 + _ [3]uint8 Master_key_identifier [16]uint8 } @@ -455,60 +456,63 @@ type Ucred struct { } type TCPInfo struct { - State uint8 - Ca_state uint8 - Retransmits uint8 - Probes uint8 - Backoff uint8 - Options uint8 - Rto uint32 - Ato uint32 - Snd_mss uint32 - Rcv_mss uint32 - Unacked uint32 - Sacked uint32 - Lost uint32 - Retrans uint32 - Fackets uint32 - Last_data_sent uint32 - Last_ack_sent uint32 - Last_data_recv uint32 - Last_ack_recv uint32 - Pmtu uint32 - Rcv_ssthresh uint32 - Rtt uint32 - Rttvar uint32 - Snd_ssthresh uint32 - Snd_cwnd uint32 - Advmss uint32 - Reordering uint32 - Rcv_rtt uint32 - Rcv_space uint32 - Total_retrans uint32 - Pacing_rate uint64 - Max_pacing_rate uint64 - Bytes_acked uint64 - Bytes_received uint64 - Segs_out uint32 - Segs_in uint32 - Notsent_bytes uint32 - Min_rtt uint32 - Data_segs_in uint32 - Data_segs_out uint32 - Delivery_rate uint64 - Busy_time uint64 - Rwnd_limited uint64 - Sndbuf_limited uint64 - Delivered uint32 - Delivered_ce uint32 - Bytes_sent uint64 - Bytes_retrans uint64 - Dsack_dups uint32 - Reord_seen uint32 - Rcv_ooopack uint32 - Snd_wnd uint32 - Rcv_wnd uint32 - Rehash uint32 + State uint8 + Ca_state uint8 + Retransmits uint8 + Probes uint8 + Backoff uint8 + Options uint8 + Rto uint32 + Ato uint32 + Snd_mss uint32 + Rcv_mss uint32 + Unacked uint32 + Sacked uint32 + Lost uint32 + Retrans uint32 + Fackets uint32 + Last_data_sent uint32 + Last_ack_sent uint32 + Last_data_recv uint32 + Last_ack_recv uint32 + Pmtu uint32 + Rcv_ssthresh uint32 + Rtt uint32 + Rttvar uint32 + Snd_ssthresh uint32 + Snd_cwnd uint32 + Advmss uint32 + Reordering uint32 + Rcv_rtt uint32 + Rcv_space uint32 + Total_retrans uint32 + Pacing_rate uint64 + Max_pacing_rate uint64 + Bytes_acked uint64 + Bytes_received uint64 + Segs_out uint32 + Segs_in uint32 + Notsent_bytes uint32 + Min_rtt uint32 + Data_segs_in uint32 + Data_segs_out uint32 + Delivery_rate uint64 + Busy_time uint64 + Rwnd_limited uint64 + Sndbuf_limited uint64 + Delivered uint32 + Delivered_ce uint32 + Bytes_sent uint64 + Bytes_retrans uint64 + Dsack_dups uint32 + Reord_seen uint32 + Rcv_ooopack uint32 + Snd_wnd uint32 + Rcv_wnd uint32 + Rehash uint32 + Total_rto uint16 + Total_rto_recoveries uint16 + Total_rto_time uint32 } type CanFilter struct { @@ -551,7 +555,7 @@ const ( SizeofIPv6MTUInfo = 0x20 SizeofICMPv6Filter = 0x20 SizeofUcred = 0xc - SizeofTCPInfo = 0xf0 + SizeofTCPInfo = 0xf8 SizeofCanFilter = 0x8 SizeofTCPRepairOpt = 0x8 ) @@ -3399,7 +3403,7 @@ const ( DEVLINK_PORT_FN_ATTR_STATE = 0x2 DEVLINK_PORT_FN_ATTR_OPSTATE = 0x3 DEVLINK_PORT_FN_ATTR_CAPS = 0x4 - DEVLINK_PORT_FUNCTION_ATTR_MAX = 0x4 + DEVLINK_PORT_FUNCTION_ATTR_MAX = 0x5 ) type FsverityDigest struct { @@ -4183,7 +4187,8 @@ const ( ) type LandlockRulesetAttr struct { - Access_fs uint64 + Access_fs uint64 + Access_net uint64 } type LandlockPathBeneathAttr struct { @@ -5134,7 +5139,7 @@ const ( NL80211_FREQUENCY_ATTR_GO_CONCURRENT = 0xf NL80211_FREQUENCY_ATTR_INDOOR_ONLY = 0xe NL80211_FREQUENCY_ATTR_IR_CONCURRENT = 0xf - NL80211_FREQUENCY_ATTR_MAX = 0x1b + NL80211_FREQUENCY_ATTR_MAX = 0x1c NL80211_FREQUENCY_ATTR_MAX_TX_POWER = 0x6 NL80211_FREQUENCY_ATTR_NO_10MHZ = 0x11 NL80211_FREQUENCY_ATTR_NO_160MHZ = 0xc @@ -5547,7 +5552,7 @@ const ( NL80211_REGDOM_TYPE_CUSTOM_WORLD = 0x2 NL80211_REGDOM_TYPE_INTERSECTION = 0x3 NL80211_REGDOM_TYPE_WORLD = 0x1 - NL80211_REG_RULE_ATTR_MAX = 0x7 + NL80211_REG_RULE_ATTR_MAX = 0x8 NL80211_REKEY_DATA_AKM = 0x4 NL80211_REKEY_DATA_KCK = 0x2 NL80211_REKEY_DATA_KEK = 0x1 diff --git a/vendor/golang.org/x/sys/windows/env_windows.go b/vendor/golang.org/x/sys/windows/env_windows.go index b8ad192..d4577a4 100644 --- a/vendor/golang.org/x/sys/windows/env_windows.go +++ b/vendor/golang.org/x/sys/windows/env_windows.go @@ -37,14 +37,17 @@ func (token Token) Environ(inheritExisting bool) (env []string, err error) { return nil, err } defer DestroyEnvironmentBlock(block) - blockp := unsafe.Pointer(block) - for { - entry := UTF16PtrToString((*uint16)(blockp)) - if len(entry) == 0 { - break + size := unsafe.Sizeof(*block) + for *block != 0 { + // find NUL terminator + end := unsafe.Pointer(block) + for *(*uint16)(end) != 0 { + end = unsafe.Add(end, size) } - env = append(env, entry) - blockp = unsafe.Add(blockp, 2*(len(entry)+1)) + + entry := unsafe.Slice(block, (uintptr(end)-uintptr(unsafe.Pointer(block)))/size) + env = append(env, UTF16ToString(entry)) + block = (*uint16)(unsafe.Add(end, size)) } return env, nil } diff --git a/vendor/golang.org/x/sys/windows/syscall_windows.go b/vendor/golang.org/x/sys/windows/syscall_windows.go index ffb8708..6395a03 100644 --- a/vendor/golang.org/x/sys/windows/syscall_windows.go +++ b/vendor/golang.org/x/sys/windows/syscall_windows.go @@ -125,8 +125,7 @@ func UTF16PtrToString(p *uint16) string { for ptr := unsafe.Pointer(p); *(*uint16)(ptr) != 0; n++ { ptr = unsafe.Pointer(uintptr(ptr) + unsafe.Sizeof(*p)) } - - return string(utf16.Decode(unsafe.Slice(p, n))) + return UTF16ToString(unsafe.Slice(p, n)) } func Getpagesize() int { return 4096 } diff --git a/vendor/modules.txt b/vendor/modules.txt index aa3663b..0b27915 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,7 +1,7 @@ # github.com/DataDog/zstd v1.5.5 ## explicit; go 1.14 github.com/DataDog/zstd -# github.com/NicoNex/echotron/v3 v3.28.0 +# github.com/NicoNex/echotron/v3 v3.29.0 ## explicit; go 1.19 github.com/NicoNex/echotron/v3 # github.com/beorn7/perks v1.0.1 @@ -34,18 +34,20 @@ github.com/cockroachdb/errors/withstack # github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b ## explicit; go 1.16 github.com/cockroachdb/logtags -# github.com/cockroachdb/pebble v1.0.0 -## explicit; go 1.19 +# github.com/cockroachdb/pebble v1.1.0 +## explicit; go 1.20 github.com/cockroachdb/pebble github.com/cockroachdb/pebble/internal/arenaskl github.com/cockroachdb/pebble/internal/base github.com/cockroachdb/pebble/internal/batchskl github.com/cockroachdb/pebble/internal/bytealloc github.com/cockroachdb/pebble/internal/cache +github.com/cockroachdb/pebble/internal/constants github.com/cockroachdb/pebble/internal/crc github.com/cockroachdb/pebble/internal/fastrand github.com/cockroachdb/pebble/internal/humanize github.com/cockroachdb/pebble/internal/intern +github.com/cockroachdb/pebble/internal/invalidating github.com/cockroachdb/pebble/internal/invariants github.com/cockroachdb/pebble/internal/keyspan github.com/cockroachdb/pebble/internal/manifest @@ -53,12 +55,15 @@ github.com/cockroachdb/pebble/internal/manual github.com/cockroachdb/pebble/internal/private github.com/cockroachdb/pebble/internal/rangedel github.com/cockroachdb/pebble/internal/rangekey -github.com/cockroachdb/pebble/internal/rate github.com/cockroachdb/pebble/internal/rawalloc github.com/cockroachdb/pebble/internal/testkeys github.com/cockroachdb/pebble/objstorage -github.com/cockroachdb/pebble/objstorage/shared -github.com/cockroachdb/pebble/objstorage/sharedobjcat +github.com/cockroachdb/pebble/objstorage/objstorageprovider +github.com/cockroachdb/pebble/objstorage/objstorageprovider/objiotracing +github.com/cockroachdb/pebble/objstorage/objstorageprovider/remoteobjcat +github.com/cockroachdb/pebble/objstorage/objstorageprovider/sharedcache +github.com/cockroachdb/pebble/objstorage/remote +github.com/cockroachdb/pebble/rangekey github.com/cockroachdb/pebble/record github.com/cockroachdb/pebble/sstable github.com/cockroachdb/pebble/vfs @@ -75,13 +80,16 @@ github.com/cockroachdb/redact/internal/markers github.com/cockroachdb/redact/internal/redact github.com/cockroachdb/redact/internal/rfmt github.com/cockroachdb/redact/internal/rfmt/fmtsort +# github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 +## explicit; go 1.19 +github.com/cockroachdb/tokenbucket # github.com/davecgh/go-spew v1.1.1 ## explicit github.com/davecgh/go-spew/spew # github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f ## explicit github.com/dgryski/go-rendezvous -# github.com/getsentry/sentry-go v0.26.0 +# github.com/getsentry/sentry-go v0.27.0 ## explicit; go 1.18 github.com/getsentry/sentry-go github.com/getsentry/sentry-go/internal/debug @@ -112,8 +120,8 @@ github.com/golang/snappy # github.com/hjson/hjson-go v3.3.0+incompatible ## explicit github.com/hjson/hjson-go -# github.com/klauspost/compress v1.17.4 -## explicit; go 1.19 +# github.com/klauspost/compress v1.17.7 +## explicit; go 1.20 github.com/klauspost/compress github.com/klauspost/compress/fse github.com/klauspost/compress/huff0 @@ -134,10 +142,10 @@ github.com/pkg/errors ## explicit; go 1.19 github.com/prometheus/client_golang/prometheus github.com/prometheus/client_golang/prometheus/internal -# github.com/prometheus/client_model v0.5.0 +# github.com/prometheus/client_model v0.6.0 ## explicit; go 1.19 github.com/prometheus/client_model/go -# github.com/prometheus/common v0.46.0 +# github.com/prometheus/common v0.48.0 ## explicit; go 1.20 github.com/prometheus/common/expfmt github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg @@ -153,10 +161,11 @@ github.com/rogpeppe/go-internal/fmtsort # github.com/sirupsen/logrus v1.9.3 ## explicit; go 1.13 github.com/sirupsen/logrus -# golang.org/x/exp v0.0.0-20240119083558-1b970713d09a +# golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 ## explicit; go 1.20 +golang.org/x/exp/constraints golang.org/x/exp/rand -# golang.org/x/sys v0.16.0 +# golang.org/x/sys v0.17.0 ## explicit; go 1.18 golang.org/x/sys/execabs golang.org/x/sys/unix